Flake8 and Black run on all files

This commit is contained in:
Keith Edmunds 2023-07-09 16:12:21 +01:00
parent f44d6aa25e
commit 986257bef6
28 changed files with 1359 additions and 1291 deletions

13
.flake8 Normal file
View File

@ -0,0 +1,13 @@
[flake8]
max-line-length = 88
select = C,E,F,W,B,B950
extend-ignore = E203, E501
exclude =
.git
app/ui,
__pycache__,
archive,
migrations,
prof,
docs,
app/icons_rc.py

View File

@ -2,14 +2,12 @@
from PyQt6.QtCore import Qt, QEvent, QObject from PyQt6.QtCore import Qt, QEvent, QObject
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QMainWindow, QMainWindow,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QStyledItemDelegate, QStyledItemDelegate,
QStyleOptionViewItem,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
) )
@ -33,16 +31,15 @@ class EscapeDelegate(QStyledItemDelegate):
if event.type() == QEvent.Type.KeyPress: if event.type() == QEvent.Type.KeyPress:
key_event = cast(QKeyEvent, event) key_event = cast(QKeyEvent, event)
if key_event.key() == Qt.Key.Key_Return: if key_event.key() == Qt.Key.Key_Return:
if key_event.modifiers() == ( if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier):
Qt.KeyboardModifier.ControlModifier
):
print("save data") print("save data")
self.commitData.emit(editor) self.commitData.emit(editor)
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
self.parent(), "Abandon edit", "Discard changes?") self.parent(), "Abandon edit", "Discard changes?"
)
if discard_edits == QMessageBox.StandardButton.Yes: if discard_edits == QMessageBox.StandardButton.Yes:
print("abandon edit") print("abandon edit")
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
@ -74,7 +71,7 @@ class MainWindow(QMainWindow):
self.table_widget.resizeRowsToContents() self.table_widget.resizeRowsToContents()
if __name__ == '__main__': if __name__ == "__main__":
app = QApplication([]) app = QApplication([])
window = MainWindow() window = MainWindow()
window.show() window.show()

View File

@ -2,7 +2,7 @@
import os import os
from pydub import AudioSegment, effects from pydub import AudioSegment
# DIR = "/home/kae/git/musicmuster/archive" # DIR = "/home/kae/git/musicmuster/archive"
DIR = "/home/kae/git/musicmuster" DIR = "/home/kae/git/musicmuster"

View File

@ -1,19 +1,18 @@
import inspect import inspect
import logging
import os import os
from config import Config from config import Config
from contextlib import contextmanager from contextlib import contextmanager
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import (sessionmaker, scoped_session) from sqlalchemy.orm import sessionmaker, scoped_session
from typing import Generator from typing import Generator
from log import log from log import log
MYSQL_CONNECT = os.environ.get('MM_DB') MYSQL_CONNECT = os.environ.get("MM_DB")
if MYSQL_CONNECT is None: if MYSQL_CONNECT is None:
raise ValueError("MYSQL_CONNECT is undefined") raise ValueError("MYSQL_CONNECT is undefined")
else: else:
dbname = MYSQL_CONNECT.split('/')[-1] dbname = MYSQL_CONNECT.split("/")[-1]
log.debug(f"Database: {dbname}") log.debug(f"Database: {dbname}")
# MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION') # MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION')
@ -31,10 +30,10 @@ else:
engine = create_engine( engine = create_engine(
MYSQL_CONNECT, MYSQL_CONNECT,
encoding='utf-8', encoding="utf-8",
echo=Config.DISPLAY_SQL, echo=Config.DISPLAY_SQL,
pool_pre_ping=True, pool_pre_ping=True,
future=True future=True,
) )
@ -47,8 +46,7 @@ def Session() -> Generator[scoped_session, None, None]:
Session = scoped_session(sessionmaker(bind=engine, future=True)) Session = scoped_session(sessionmaker(bind=engine, future=True))
log.debug(f"SqlA: session acquired [{hex(id(Session))}]") log.debug(f"SqlA: session acquired [{hex(id(Session))}]")
log.debug( log.debug(
f"Session acquisition: {file}:{function}:{lineno} " f"Session acquisition: {file}:{function}:{lineno} " f"[{hex(id(Session))}]"
f"[{hex(id(Session))}]"
) )
yield Session yield Session
log.debug(f" SqlA: session released [{hex(id(Session))}]") log.debug(f" SqlA: session released [{hex(id(Session))}]")

View File

@ -1,4 +1,3 @@
import numpy as np
import os import os
import psutil import psutil
import shutil import shutil
@ -10,13 +9,13 @@ from config import Config
from datetime import datetime from datetime import datetime
from email.message import EmailMessage from email.message import EmailMessage
from log import log from log import log
from mutagen.flac import FLAC # type: ignore from mutagen.flac import FLAC # type: ignore
from mutagen.mp3 import MP3 # type: ignore from mutagen.mp3 import MP3 # type: ignore
from pydub import AudioSegment, effects from pydub import AudioSegment, effects
from pydub.utils import mediainfo from pydub.utils import mediainfo
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
from tinytag import TinyTag # type: ignore from tinytag import TinyTag # type: ignore
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool: def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
@ -26,7 +25,8 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
dlg.setWindowTitle(title) dlg.setWindowTitle(title)
dlg.setText(question) dlg.setText(question)
dlg.setStandardButtons( dlg.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
dlg.setIcon(QMessageBox.Icon.Question) dlg.setIcon(QMessageBox.Icon.Question)
if default_yes: if default_yes:
dlg.setDefaultButton(QMessageBox.StandardButton.Yes) dlg.setDefaultButton(QMessageBox.StandardButton.Yes)
@ -36,8 +36,10 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
def fade_point( def fade_point(
audio_segment: AudioSegment, fade_threshold: float = 0.0, audio_segment: AudioSegment,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: fade_threshold: float = 0.0,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
) -> int:
""" """
Returns the millisecond/index of the point where the volume drops below Returns the millisecond/index of the point where the volume drops below
the maximum and doesn't get louder again. the maximum and doesn't get louder again.
@ -55,8 +57,9 @@ def fade_point(
fade_threshold = max_vol fade_threshold = max_vol
while ( while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
and trim_ms > 0): # noqa W503 and trim_ms > 0
): # noqa W503
trim_ms -= chunk_size trim_ms -= chunk_size
# if there is no trailing silence, return lenght of track (it's less # if there is no trailing silence, return lenght of track (it's less
@ -77,10 +80,10 @@ def file_is_unreadable(path: Optional[str]) -> bool:
def get_audio_segment(path: str) -> Optional[AudioSegment]: def get_audio_segment(path: str) -> Optional[AudioSegment]:
try: try:
if path.endswith('.mp3'): if path.endswith(".mp3"):
return AudioSegment.from_mp3(path) return AudioSegment.from_mp3(path)
elif path.endswith('.flac'): elif path.endswith(".flac"):
return AudioSegment.from_file(path, "flac") # type: ignore return AudioSegment.from_file(path, "flac") # type: ignore
except AttributeError: except AttributeError:
return None return None
@ -99,12 +102,13 @@ def get_tags(path: str) -> Dict[str, Any]:
artist=tag.artist, artist=tag.artist,
bitrate=round(tag.bitrate), bitrate=round(tag.bitrate),
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000), duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
path=path path=path,
) )
def get_relative_date(past_date: Optional[datetime], def get_relative_date(
reference_date: Optional[datetime] = None) -> str: past_date: Optional[datetime], reference_date: Optional[datetime] = None
) -> str:
""" """
Return how long before reference_date past_date is as string. Return how long before reference_date past_date is as string.
@ -145,9 +149,10 @@ def get_relative_date(past_date: Optional[datetime],
def leading_silence( def leading_silence(
audio_segment: AudioSegment, audio_segment: AudioSegment,
silence_threshold: int = Config.DBFS_SILENCE, silence_threshold: int = Config.DBFS_SILENCE,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
) -> int:
""" """
Returns the millisecond/index that the leading silence ends. Returns the millisecond/index that the leading silence ends.
audio_segment - the segment to find silence in audio_segment - the segment to find silence in
@ -159,9 +164,11 @@ def leading_silence(
trim_ms: int = 0 # ms trim_ms: int = 0 # ms
assert chunk_size > 0 # to avoid infinite loop assert chunk_size > 0 # to avoid infinite loop
while ( while audio_segment[
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 trim_ms : trim_ms + chunk_size
silence_threshold and trim_ms < len(audio_segment)): ].dBFS < silence_threshold and trim_ms < len( # noqa W504
audio_segment
):
trim_ms += chunk_size trim_ms += chunk_size
# if there is no end it should return the length of the segment # if there is no end it should return the length of the segment
@ -175,9 +182,9 @@ def send_mail(to_addr, from_addr, subj, body):
msg = EmailMessage() msg = EmailMessage()
msg.set_content(body) msg.set_content(body)
msg['Subject'] = subj msg["Subject"] = subj
msg['From'] = from_addr msg["From"] = from_addr
msg['To'] = to_addr msg["To"] = to_addr
# Send the message via SMTP server. # Send the message via SMTP server.
context = ssl.create_default_context() context = ssl.create_default_context()
@ -194,8 +201,7 @@ def send_mail(to_addr, from_addr, subj, body):
s.quit() s.quit()
def ms_to_mmss(ms: Optional[int], decimals: int = 0, def ms_to_mmss(ms: Optional[int], decimals: int = 0, negative: bool = False) -> str:
negative: bool = False) -> str:
"""Convert milliseconds to mm:ss""" """Convert milliseconds to mm:ss"""
minutes: int minutes: int
@ -227,13 +233,12 @@ def normalise_track(path):
# Check type # Check type
ftype = os.path.splitext(path)[1][1:] ftype = os.path.splitext(path)[1][1:]
if ftype not in ['mp3', 'flac']: if ftype not in ["mp3", "flac"]:
log.info( log.info(
f"helpers.normalise_track({path}): " f"helpers.normalise_track({path}): " f"File type {ftype} not implemented"
f"File type {ftype} not implemented"
) )
bitrate = mediainfo(path)['bit_rate'] bitrate = mediainfo(path)["bit_rate"]
audio = get_audio_segment(path) audio = get_audio_segment(path)
if not audio: if not audio:
return return
@ -245,23 +250,20 @@ def normalise_track(path):
_, temp_path = tempfile.mkstemp() _, temp_path = tempfile.mkstemp()
shutil.copyfile(path, temp_path) shutil.copyfile(path, temp_path)
except Exception as err: except Exception as err:
log.debug( log.debug(f"helpers.normalise_track({path}): err1: {repr(err)}")
f"helpers.normalise_track({path}): err1: {repr(err)}"
)
return return
# Overwrite original file with normalised output # Overwrite original file with normalised output
normalised = effects.normalize(audio) normalised = effects.normalize(audio)
try: try:
normalised.export(path, format=os.path.splitext(path)[1][1:], normalised.export(path, format=os.path.splitext(path)[1][1:], bitrate=bitrate)
bitrate=bitrate)
# Fix up permssions and ownership # Fix up permssions and ownership
os.chown(path, stats.st_uid, stats.st_gid) os.chown(path, stats.st_uid, stats.st_gid)
os.chmod(path, stats.st_mode) os.chmod(path, stats.st_mode)
# Copy tags # Copy tags
if ftype == 'flac': if ftype == "flac":
tag_handler = FLAC tag_handler = FLAC
elif ftype == 'mp3': elif ftype == "mp3":
tag_handler = MP3 tag_handler = MP3
else: else:
return return
@ -271,9 +273,7 @@ def normalise_track(path):
dst[tag] = src[tag] dst[tag] = src[tag]
dst.save() dst.save()
except Exception as err: except Exception as err:
log.debug( log.debug(f"helpers.normalise_track({path}): err2: {repr(err)}")
f"helpers.normalise_track({path}): err2: {repr(err)}"
)
# Restore original file # Restore original file
shutil.copyfile(path, temp_path) shutil.copyfile(path, temp_path)
finally: finally:
@ -296,9 +296,9 @@ def open_in_audacity(path: str) -> bool:
if not path: if not path:
return False return False
to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) to_pipe: str = "/tmp/audacity_script_pipe.to." + str(os.getuid())
from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) from_pipe: str = "/tmp/audacity_script_pipe.from." + str(os.getuid())
eol: str = '\n' eol: str = "\n"
def send_command(command: str) -> None: def send_command(command: str) -> None:
"""Send a single command.""" """Send a single command."""
@ -308,13 +308,13 @@ def open_in_audacity(path: str) -> bool:
def get_response() -> str: def get_response() -> str:
"""Return the command response.""" """Return the command response."""
result: str = '' result: str = ""
line: str = '' line: str = ""
while True: while True:
result += line result += line
line = from_audacity.readline() line = from_audacity.readline()
if line == '\n' and len(result) > 0: if line == "\n" and len(result) > 0:
break break
return result return result
@ -325,8 +325,7 @@ def open_in_audacity(path: str) -> bool:
response = get_response() response = get_response()
return response return response
with open(to_pipe, 'w') as to_audacity, open( with open(to_pipe, "w") as to_audacity, open(from_pipe, "rt") as from_audacity:
from_pipe, 'rt') as from_audacity:
do_command(f'Import2: Filename="{path}"') do_command(f'Import2: Filename="{path}"')
return True return True
@ -338,18 +337,18 @@ def set_track_metadata(session, track):
t = get_tags(track.path) t = get_tags(track.path)
audio = get_audio_segment(track.path) audio = get_audio_segment(track.path)
track.title = t['title'] track.title = t["title"]
track.artist = t['artist'] track.artist = t["artist"]
track.bitrate = t['bitrate'] track.bitrate = t["bitrate"]
if not audio: if not audio:
return return
track.duration = len(audio) track.duration = len(audio)
track.start_gap = leading_silence(audio) track.start_gap = leading_silence(audio)
track.fade_at = round(fade_point(audio) / 1000, track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
Config.MILLISECOND_SIGFIGS) * 1000 track.silence_at = (
track.silence_at = round(trailing_silence(audio) / 1000, round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
Config.MILLISECOND_SIGFIGS) * 1000 )
track.mtime = os.path.getmtime(track.path) track.mtime = os.path.getmtime(track.path)
session.commit() session.commit()
@ -358,20 +357,20 @@ def set_track_metadata(session, track):
def show_OK(parent: QMainWindow, title: str, msg: str) -> None: def show_OK(parent: QMainWindow, title: str, msg: str) -> None:
"""Display a message to user""" """Display a message to user"""
QMessageBox.information(parent, title, msg, QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok)
buttons=QMessageBox.StandardButton.Ok)
def show_warning(parent: QMainWindow, title: str, msg: str) -> None: def show_warning(parent: QMainWindow, title: str, msg: str) -> None:
"""Display a warning to user""" """Display a warning to user"""
QMessageBox.warning(parent, title, msg, QMessageBox.warning(parent, title, msg, buttons=QMessageBox.StandardButton.Cancel)
buttons=QMessageBox.StandardButton.Cancel)
def trailing_silence( def trailing_silence(
audio_segment: AudioSegment, silence_threshold: int = -50, audio_segment: AudioSegment,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: silence_threshold: int = -50,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
) -> int:
"""Return fade point from start in milliseconds""" """Return fade point from start in milliseconds"""
return fade_point(audio_segment, silence_threshold, chunk_size) return fade_point(audio_segment, silence_threshold, chunk_size)

View File

@ -1,9 +1,9 @@
import urllib.parse import urllib.parse
from datetime import datetime from datetime import datetime
from slugify import slugify # type: ignore from slugify import slugify # type: ignore
from typing import Dict, Optional from typing import Dict
from PyQt6.QtCore import QUrl # type: ignore from PyQt6.QtCore import QUrl # type: ignore
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QTabWidget from PyQt6.QtWidgets import QTabWidget
from config import Config from config import Config
@ -47,13 +47,11 @@ class InfoTabs(QTabWidget):
if url in self.tabtitles.values(): if url in self.tabtitles.values():
self.setCurrentIndex( self.setCurrentIndex(
list(self.tabtitles.keys())[ list(self.tabtitles.keys())[list(self.tabtitles.values()).index(url)]
list(self.tabtitles.values()).index(url)
]
) )
return return
short_title = title[:Config.INFO_TAB_TITLE_LENGTH] short_title = title[: Config.INFO_TAB_TITLE_LENGTH]
if self.count() < Config.MAX_INFO_TABS: if self.count() < Config.MAX_INFO_TABS:
# Create a new tab # Create a new tab
@ -63,9 +61,7 @@ class InfoTabs(QTabWidget):
else: else:
# Reuse oldest widget # Reuse oldest widget
widget = min( widget = min(self.last_update, key=self.last_update.get) # type: ignore
self.last_update, key=self.last_update.get # type: ignore
)
tab_index = self.indexOf(widget) tab_index = self.indexOf(widget)
self.setTabText(tab_index, short_title) self.setTabText(tab_index, short_title)

View File

@ -1,13 +1,11 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os.path
import re import re
import stackprinter # type: ignore
from dbconfig import Session, scoped_session from dbconfig import scoped_session
from datetime import datetime from datetime import datetime
from typing import Iterable, List, Optional, Union, ValuesView from typing import List, Optional
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@ -35,21 +33,14 @@ from sqlalchemy.orm.exc import (
from sqlalchemy.exc import ( from sqlalchemy.exc import (
IntegrityError, IntegrityError,
) )
from config import Config
from helpers import (
fade_point,
get_audio_segment,
get_tags,
leading_silence,
trailing_silence,
)
from log import log from log import log
Base = declarative_base() Base = declarative_base()
# Database classes # Database classes
class Carts(Base): class Carts(Base):
__tablename__ = 'carts' __tablename__ = "carts"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
cart_number: int = Column(Integer, nullable=False, unique=True) cart_number: int = Column(Integer, nullable=False, unique=True)
@ -64,10 +55,15 @@ class Carts(Base):
f"name={self.name}, path={self.path}>" f"name={self.name}, path={self.path}>"
) )
def __init__(self, session: scoped_session, cart_number: int, def __init__(
name: Optional[str] = None, self,
duration: Optional[int] = None, path: Optional[str] = None, session: scoped_session,
enabled: bool = True) -> None: cart_number: int,
name: Optional[str] = None,
duration: Optional[int] = None,
path: Optional[str] = None,
enabled: bool = True,
) -> None:
"""Create new cart""" """Create new cart"""
self.cart_number = cart_number self.cart_number = cart_number
@ -81,7 +77,7 @@ class Carts(Base):
class NoteColours(Base): class NoteColours(Base):
__tablename__ = 'notecolours' __tablename__ = "notecolours"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
substring = Column(String(256), index=False) substring = Column(String(256), index=False)
@ -106,11 +102,15 @@ class NoteColours(Base):
if not text: if not text:
return None return None
for rec in session.execute( for rec in (
session.execute(
select(NoteColours) select(NoteColours)
.filter(NoteColours.enabled.is_(True)) .filter(NoteColours.enabled.is_(True))
.order_by(NoteColours.order) .order_by(NoteColours.order)
).scalars().all(): )
.scalars()
.all()
):
if rec.is_regex: if rec.is_regex:
flags = re.UNICODE flags = re.UNICODE
if not rec.is_casesensitive: if not rec.is_casesensitive:
@ -130,11 +130,11 @@ class NoteColours(Base):
class Playdates(Base): class Playdates(Base):
__tablename__ = 'playdates' __tablename__ = "playdates"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
lastplayed = Column(DateTime, index=True, default=None) lastplayed = Column(DateTime, index=True, default=None)
track_id = Column(Integer, ForeignKey('tracks.id')) track_id = Column(Integer, ForeignKey("tracks.id"))
track: "Tracks" = relationship("Tracks", back_populates="playdates") track: "Tracks" = relationship("Tracks", back_populates="playdates")
def __repr__(self) -> str: def __repr__(self) -> str:
@ -152,8 +152,7 @@ class Playdates(Base):
session.commit() session.commit()
@staticmethod @staticmethod
def last_played(session: scoped_session, def last_played(session: scoped_session, track_id: int) -> Optional[datetime]:
track_id: int) -> Optional[datetime]:
"""Return datetime track last played or None""" """Return datetime track last played or None"""
last_played = session.execute( last_played = session.execute(
@ -169,8 +168,7 @@ class Playdates(Base):
return None return None
@staticmethod @staticmethod
def played_after(session: scoped_session, def played_after(session: scoped_session, since: datetime) -> List["Playdates"]:
since: datetime) -> List["Playdates"]:
"""Return a list of Playdates objects since passed time""" """Return a list of Playdates objects since passed time"""
return ( return (
@ -203,7 +201,7 @@ class Playlists(Base):
"PlaylistRows", "PlaylistRows",
back_populates="playlist", back_populates="playlist",
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="PlaylistRows.plr_rownum" order_by="PlaylistRows.plr_rownum",
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@ -232,11 +230,9 @@ class Playlists(Base):
) )
@classmethod @classmethod
def create_playlist_from_template(cls, def create_playlist_from_template(
session: scoped_session, cls, session: scoped_session, template: "Playlists", playlist_name: str
template: "Playlists", ) -> Optional["Playlists"]:
playlist_name: str) \
-> Optional["Playlists"]:
"""Create a new playlist from template""" """Create a new playlist from template"""
playlist = cls(session, playlist_name) playlist = cls(session, playlist_name)
@ -277,9 +273,7 @@ class Playlists(Base):
return ( return (
session.execute( session.execute(
select(cls) select(cls).filter(cls.is_template.is_(True)).order_by(cls.name)
.filter(cls.is_template.is_(True))
.order_by(cls.name)
) )
.scalars() .scalars()
.all() .all()
@ -295,7 +289,7 @@ class Playlists(Base):
.filter( .filter(
cls.tab.is_(None), cls.tab.is_(None),
cls.is_template.is_(False), cls.is_template.is_(False),
cls.deleted.is_(False) cls.deleted.is_(False),
) )
.order_by(cls.last_used.desc()) .order_by(cls.last_used.desc())
) )
@ -310,11 +304,7 @@ class Playlists(Base):
""" """
return ( return (
session.execute( session.execute(select(cls).where(cls.tab.is_not(None)).order_by(cls.tab))
select(cls)
.where(cls.tab.is_not(None))
.order_by(cls.tab)
)
.scalars() .scalars()
.all() .all()
) )
@ -329,15 +319,9 @@ class Playlists(Base):
def move_tab(session: scoped_session, frm: int, to: int) -> None: def move_tab(session: scoped_session, frm: int, to: int) -> None:
"""Move tabs""" """Move tabs"""
row_frm = session.execute( row_frm = session.execute(select(Playlists).filter_by(tab=frm)).scalar_one()
select(Playlists)
.filter_by(tab=frm)
).scalar_one()
row_to = session.execute( row_to = session.execute(select(Playlists).filter_by(tab=to)).scalar_one()
select(Playlists)
.filter_by(tab=to)
).scalar_one()
row_frm.tab = None row_frm.tab = None
row_to.tab = None row_to.tab = None
@ -354,8 +338,9 @@ class Playlists(Base):
session.flush() session.flush()
@staticmethod @staticmethod
def save_as_template(session: scoped_session, def save_as_template(
playlist_id: int, template_name: str) -> None: session: scoped_session, playlist_id: int, template_name: str
) -> None:
"""Save passed playlist as new template""" """Save passed playlist as new template"""
template = Playlists(session, template_name) template = Playlists(session, template_name)
@ -369,15 +354,14 @@ class Playlists(Base):
class PlaylistRows(Base): class PlaylistRows(Base):
__tablename__ = 'playlist_rows' __tablename__ = "playlist_rows"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
plr_rownum: int = Column(Integer, nullable=False) plr_rownum: int = Column(Integer, nullable=False)
note: str = Column(String(2048), index=False, default="", nullable=False) note: str = Column(String(2048), index=False, default="", nullable=False)
playlist_id: int = Column(Integer, ForeignKey('playlists.id'), playlist_id: int = Column(Integer, ForeignKey("playlists.id"), nullable=False)
nullable=False)
playlist: Playlists = relationship(Playlists, back_populates="rows") playlist: Playlists = relationship(Playlists, back_populates="rows")
track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True) track_id = Column(Integer, ForeignKey("tracks.id"), nullable=True)
track: "Tracks" = relationship("Tracks", back_populates="playlistrows") track: "Tracks" = relationship("Tracks", back_populates="playlistrows")
played: bool = Column(Boolean, nullable=False, index=False, default=False) played: bool = Column(Boolean, nullable=False, index=False, default=False)
@ -388,13 +372,14 @@ class PlaylistRows(Base):
f"note={self.note}, plr_rownum={self.plr_rownum}>" f"note={self.note}, plr_rownum={self.plr_rownum}>"
) )
def __init__(self, def __init__(
session: scoped_session, self,
playlist_id: int, session: scoped_session,
track_id: Optional[int], playlist_id: int,
row_number: int, track_id: Optional[int],
note: str = "" row_number: int,
) -> None: note: str = "",
) -> None:
"""Create PlaylistRows object""" """Create PlaylistRows object"""
self.playlist_id = playlist_id self.playlist_id = playlist_id
@ -409,38 +394,38 @@ class PlaylistRows(Base):
current_note = self.note current_note = self.note
if current_note: if current_note:
self.note = current_note + '\n' + extra_note self.note = current_note + "\n" + extra_note
else: else:
self.note = extra_note self.note = extra_note
@staticmethod @staticmethod
def copy_playlist(session: scoped_session, def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None:
src_id: int,
dst_id: int) -> None:
"""Copy playlist entries""" """Copy playlist entries"""
src_rows = session.execute( src_rows = (
select(PlaylistRows) session.execute(
.filter(PlaylistRows.playlist_id == src_id) select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id)
).scalars().all() )
.scalars()
.all()
)
for plr in src_rows: for plr in src_rows:
PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, plr.note)
plr.note)
@staticmethod @staticmethod
def delete_higher_rows( def delete_higher_rows(
session: scoped_session, playlist_id: int, maxrow: int) -> None: session: scoped_session, playlist_id: int, maxrow: int
) -> None:
""" """
Delete rows in given playlist that have a higher row number Delete rows in given playlist that have a higher row number
than 'maxrow' than 'maxrow'
""" """
session.execute( session.execute(
delete(PlaylistRows) delete(PlaylistRows).where(
.where(
PlaylistRows.playlist_id == playlist_id, PlaylistRows.playlist_id == playlist_id,
PlaylistRows.plr_rownum > maxrow PlaylistRows.plr_rownum > maxrow,
) )
) )
session.flush() session.flush()
@ -451,11 +436,15 @@ class PlaylistRows(Base):
Ensure the row numbers for passed playlist have no gaps Ensure the row numbers for passed playlist have no gaps
""" """
plrs = session.execute( plrs = (
select(PlaylistRows) session.execute(
.where(PlaylistRows.playlist_id == playlist_id) select(PlaylistRows)
.order_by(PlaylistRows.plr_rownum) .where(PlaylistRows.playlist_id == playlist_id)
).scalars().all() .order_by(PlaylistRows.plr_rownum)
)
.scalars()
.all()
)
for i, plr in enumerate(plrs): for i, plr in enumerate(plrs):
plr.plr_rownum = i plr.plr_rownum = i
@ -464,113 +453,126 @@ class PlaylistRows(Base):
session.commit() session.commit()
@classmethod @classmethod
def get_from_id_list(cls, session: scoped_session, playlist_id: int, def get_from_id_list(
plr_ids: List[int]) -> List["PlaylistRows"]: cls, session: scoped_session, playlist_id: int, plr_ids: List[int]
) -> List["PlaylistRows"]:
""" """
Take a list of PlaylistRows ids and return a list of corresponding Take a list of PlaylistRows ids and return a list of corresponding
PlaylistRows objects PlaylistRows objects
""" """
plrs = session.execute( plrs = (
select(cls) session.execute(
.where( select(cls)
cls.playlist_id == playlist_id, .where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
cls.id.in_(plr_ids) .order_by(cls.plr_rownum)
).order_by(cls.plr_rownum)).scalars().all() )
.scalars()
.all()
)
return plrs return plrs
@staticmethod @staticmethod
def get_last_used_row(session: scoped_session, def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]:
playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows""" """Return the last used row for playlist, or None if no rows"""
return session.execute( return session.execute(
select(func.max(PlaylistRows.plr_rownum)) select(func.max(PlaylistRows.plr_rownum)).where(
.where(PlaylistRows.playlist_id == playlist_id) PlaylistRows.playlist_id == playlist_id
)
).scalar_one() ).scalar_one()
@staticmethod @staticmethod
def get_track_plr(session: scoped_session, track_id: int, def get_track_plr(
playlist_id: int) -> Optional["PlaylistRows"]: session: scoped_session, track_id: int, playlist_id: int
) -> Optional["PlaylistRows"]:
"""Return first matching PlaylistRows object or None""" """Return first matching PlaylistRows object or None"""
return session.scalars( return session.scalars(
select(PlaylistRows) select(PlaylistRows)
.where( .where(
PlaylistRows.track_id == track_id, PlaylistRows.track_id == track_id,
PlaylistRows.playlist_id == playlist_id PlaylistRows.playlist_id == playlist_id,
) )
.limit(1) .limit(1)
).first() ).first()
@classmethod @classmethod
def get_played_rows(cls, session: scoped_session, def get_played_rows(
playlist_id: int) -> List["PlaylistRows"]: cls, session: scoped_session, playlist_id: int
) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of rows that For passed playlist, return a list of rows that
have been played. have been played.
""" """
plrs = session.execute( plrs = (
select(cls) session.execute(
.where( select(cls)
cls.playlist_id == playlist_id, .where(cls.playlist_id == playlist_id, cls.played.is_(True))
cls.played.is_(True) .order_by(cls.plr_rownum)
) )
.order_by(cls.plr_rownum) .scalars()
).scalars().all() .all()
)
return plrs return plrs
@classmethod @classmethod
def get_rows_with_tracks( def get_rows_with_tracks(
cls, session: scoped_session, playlist_id: int, cls,
session: scoped_session,
playlist_id: int,
from_row: Optional[int] = None, from_row: Optional[int] = None,
to_row: Optional[int] = None) -> List["PlaylistRows"]: to_row: Optional[int] = None,
) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of rows that For passed playlist, return a list of rows that
contain tracks contain tracks
""" """
query = select(cls).where( query = select(cls).where(
cls.playlist_id == playlist_id, cls.playlist_id == playlist_id, cls.track_id.is_not(None)
cls.track_id.is_not(None)
) )
if from_row is not None: if from_row is not None:
query = query.where(cls.plr_rownum >= from_row) query = query.where(cls.plr_rownum >= from_row)
if to_row is not None: if to_row is not None:
query = query.where(cls.plr_rownum <= to_row) query = query.where(cls.plr_rownum <= to_row)
plrs = ( plrs = session.execute((query).order_by(cls.plr_rownum)).scalars().all()
session.execute((query).order_by(cls.plr_rownum)).scalars().all()
)
return plrs return plrs
@classmethod @classmethod
def get_unplayed_rows(cls, session: scoped_session, def get_unplayed_rows(
playlist_id: int) -> List["PlaylistRows"]: cls, session: scoped_session, playlist_id: int
) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of playlist rows that For passed playlist, return a list of playlist rows that
have not been played. have not been played.
""" """
plrs = session.execute( plrs = (
select(cls) session.execute(
.where( select(cls)
cls.playlist_id == playlist_id, .where(
cls.track_id.is_not(None), cls.playlist_id == playlist_id,
cls.played.is_(False) cls.track_id.is_not(None),
cls.played.is_(False),
)
.order_by(cls.plr_rownum)
) )
.order_by(cls.plr_rownum) .scalars()
).scalars().all() .all()
)
return plrs return plrs
@staticmethod @staticmethod
def move_rows_down(session: scoped_session, playlist_id: int, def move_rows_down(
starting_row: int, move_by: int) -> None: session: scoped_session, playlist_id: int, starting_row: int, move_by: int
) -> None:
""" """
Create space to insert move_by additional rows by incremented row Create space to insert move_by additional rows by incremented row
number from starting_row to end of playlist number from starting_row to end of playlist
@ -580,7 +582,7 @@ class PlaylistRows(Base):
update(PlaylistRows) update(PlaylistRows)
.where( .where(
(PlaylistRows.playlist_id == playlist_id), (PlaylistRows.playlist_id == playlist_id),
(PlaylistRows.plr_rownum >= starting_row) (PlaylistRows.plr_rownum >= starting_row),
) )
.values(plr_rownum=PlaylistRows.plr_rownum + move_by) .values(plr_rownum=PlaylistRows.plr_rownum + move_by)
) )
@ -589,7 +591,7 @@ class PlaylistRows(Base):
class Settings(Base): class Settings(Base):
"""Manage settings""" """Manage settings"""
__tablename__ = 'settings' __tablename__ = "settings"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
name: str = Column(String(64), nullable=False, unique=True) name: str = Column(String(64), nullable=False, unique=True)
@ -602,21 +604,16 @@ class Settings(Base):
return f"<Settings(id={self.id}, name={self.name}, {value=}>" return f"<Settings(id={self.id}, name={self.name}, {value=}>"
def __init__(self, session: scoped_session, name: str): def __init__(self, session: scoped_session, name: str):
self.name = name self.name = name
session.add(self) session.add(self)
session.flush() session.flush()
@classmethod @classmethod
def get_int_settings(cls, session: scoped_session, def get_int_settings(cls, session: scoped_session, name: str) -> "Settings":
name: str) -> "Settings":
"""Get setting for an integer or return new setting record""" """Get setting for an integer or return new setting record"""
try: try:
return session.execute( return session.execute(select(cls).where(cls.name == name)).scalar_one()
select(cls)
.where(cls.name == name)
).scalar_one()
except NoResultFound: except NoResultFound:
return Settings(session, name) return Settings(session, name)
@ -629,7 +626,7 @@ class Settings(Base):
class Tracks(Base): class Tracks(Base):
__tablename__ = 'tracks' __tablename__ = "tracks"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(256), index=True) title = Column(String(256), index=True)
@ -641,8 +638,7 @@ class Tracks(Base):
path: str = Column(String(2048), index=False, nullable=False, unique=True) path: str = Column(String(2048), index=False, nullable=False, unique=True)
mtime = Column(Float, index=True) mtime = Column(Float, index=True)
bitrate = Column(Integer, nullable=True, default=None) bitrate = Column(Integer, nullable=True, default=None)
playlistrows: PlaylistRows = relationship("PlaylistRows", playlistrows: PlaylistRows = relationship("PlaylistRows", back_populates="track")
back_populates="track")
playlists = association_proxy("playlistrows", "playlist") playlists = association_proxy("playlistrows", "playlist")
playdates: Playdates = relationship("Playdates", back_populates="track") playdates: Playdates = relationship("Playdates", back_populates="track")
@ -653,18 +649,18 @@ class Tracks(Base):
) )
def __init__( def __init__(
self, self,
session: scoped_session, session: scoped_session,
path: str, path: str,
title: Optional[str] = None, title: Optional[str] = None,
artist: Optional[str] = None, artist: Optional[str] = None,
duration: int = 0, duration: int = 0,
start_gap: int = 0, start_gap: int = 0,
fade_at: Optional[int] = None, fade_at: Optional[int] = None,
silence_at: Optional[int] = None, silence_at: Optional[int] = None,
mtime: Optional[float] = None, mtime: Optional[float] = None,
lastplayed: Optional[datetime] = None, lastplayed: Optional[datetime] = None,
) -> None: ) -> None:
self.path = path self.path = path
self.title = title self.title = title
self.artist = artist self.artist = artist
@ -693,46 +689,36 @@ class Tracks(Base):
return session.execute(select(cls)).scalars().all() return session.execute(select(cls)).scalars().all()
@classmethod @classmethod
def get_by_path(cls, session: scoped_session, def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]:
path: str) -> Optional["Tracks"]:
""" """
Return track with passed path, or None. Return track with passed path, or None.
""" """
try: try:
return ( return session.execute(
session.execute( select(Tracks).where(Tracks.path == path)
select(Tracks) ).scalar_one()
.where(Tracks.path == path)
).scalar_one()
)
except NoResultFound: except NoResultFound:
return None return None
@classmethod @classmethod
def search_artists(cls, session: scoped_session, def search_artists(cls, session: scoped_session, text: str) -> List["Tracks"]:
text: str) -> List["Tracks"]:
"""Search case-insenstively for artists containing str""" """Search case-insenstively for artists containing str"""
return ( return (
session.execute( session.execute(
select(cls) select(cls).where(cls.artist.ilike(f"%{text}%")).order_by(cls.title)
.where(cls.artist.ilike(f"%{text}%"))
.order_by(cls.title)
) )
.scalars() .scalars()
.all() .all()
) )
@classmethod @classmethod
def search_titles(cls, session: scoped_session, def search_titles(cls, session: scoped_session, text: str) -> List["Tracks"]:
text: str) -> List["Tracks"]:
"""Search case-insenstively for titles containing str""" """Search case-insenstively for titles containing str"""
return ( return (
session.execute( session.execute(
select(cls) select(cls).where(cls.title.like(f"{text}%")).order_by(cls.title)
.where(cls.title.like(f"{text}%"))
.order_by(cls.title)
) )
.scalars() .scalars()
.all() .all()

View File

@ -1,16 +1,16 @@
# import os # import os
import threading import threading
import vlc # type: ignore import vlc # type: ignore
# #
from config import Config from config import Config
from datetime import datetime
from helpers import file_is_unreadable from helpers import file_is_unreadable
from typing import Optional from typing import Optional
from time import sleep from time import sleep
from log import log from log import log
from PyQt6.QtCore import ( # type: ignore from PyQt6.QtCore import ( # type: ignore
QRunnable, QRunnable,
QThreadPool, QThreadPool,
) )
@ -19,7 +19,6 @@ lock = threading.Lock()
class FadeTrack(QRunnable): class FadeTrack(QRunnable):
def __init__(self, player: vlc.MediaPlayer) -> None: def __init__(self, player: vlc.MediaPlayer) -> None:
super().__init__() super().__init__()
self.player = player self.player = player
@ -47,8 +46,7 @@ class FadeTrack(QRunnable):
for i in range(1, steps + 1): for i in range(1, steps + 1):
measures_to_reduce_by += i measures_to_reduce_by += i
volume_factor = 1 - ( volume_factor = 1 - (measures_to_reduce_by / total_measures_count)
measures_to_reduce_by / total_measures_count)
self.player.audio_set_volume(int(original_volume * volume_factor)) self.player.audio_set_volume(int(original_volume * volume_factor))
sleep(sleep_time) sleep(sleep_time)
@ -98,8 +96,7 @@ class Music:
return None return None
return self.player.get_position() return self.player.get_position()
def play(self, path: str, def play(self, path: str, position: Optional[float] = None) -> Optional[int]:
position: Optional[float] = None) -> Optional[int]:
""" """
Start playing the track at path. Start playing the track at path.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,12 @@
# parent (eg, bettet bitrate). # parent (eg, bettet bitrate).
import os import os
import pydymenu # type: ignore import pydymenu # type: ignore
import shutil import shutil
import sys import sys
from helpers import ( from helpers import (
fade_point,
get_audio_segment,
get_tags, get_tags,
leading_silence,
trailing_silence,
set_track_metadata, set_track_metadata,
) )
@ -29,7 +25,7 @@ process_tag_matches = True
do_processing = True do_processing = True
process_no_matches = True process_no_matches = True
source_dir = '/home/kae/music/Singles/tmp' source_dir = "/home/kae/music/Singles/tmp"
parent_dir = os.path.dirname(source_dir) parent_dir = os.path.dirname(source_dir)
# ######################################################### # #########################################################
@ -46,7 +42,7 @@ def main():
# We only want to run this against the production database because # We only want to run this against the production database because
# we will affect files in the common pool of tracks used by all # we will affect files in the common pool of tracks used by all
# databases # databases
if 'musicmuster_prod' not in os.environ.get('MM_DB'): if "musicmuster_prod" not in os.environ.get("MM_DB"):
response = input("Not on production database - c to continue: ") response = input("Not on production database - c to continue: ")
if response != "c": if response != "c":
sys.exit(0) sys.exit(0)
@ -68,8 +64,8 @@ def main():
artists_to_path = {} artists_to_path = {}
for k, v in parents.items(): for k, v in parents.items():
try: try:
titles_to_path[v['title'].lower()] = k titles_to_path[v["title"].lower()] = k
artists_to_path[v['artist'].lower()] = k artists_to_path[v["artist"].lower()] = k
except AttributeError: except AttributeError:
continue continue
@ -78,44 +74,43 @@ def main():
if not os.path.isfile(new_path): if not os.path.isfile(new_path):
continue continue
new_tags = get_tags(new_path) new_tags = get_tags(new_path)
new_title = new_tags['title'] new_title = new_tags["title"]
new_artist = new_tags['artist'] new_artist = new_tags["artist"]
bitrate = new_tags['bitrate'] bitrate = new_tags["bitrate"]
# If same filename exists in parent direcory, check tags # If same filename exists in parent direcory, check tags
parent_path = os.path.join(parent_dir, new_fname) parent_path = os.path.join(parent_dir, new_fname)
if os.path.exists(parent_path): if os.path.exists(parent_path):
parent_tags = get_tags(parent_path) parent_tags = get_tags(parent_path)
parent_title = parent_tags['title'] parent_title = parent_tags["title"]
parent_artist = parent_tags['artist'] parent_artist = parent_tags["artist"]
if ( if (str(parent_title).lower() == str(new_title).lower()) and (
(str(parent_title).lower() == str(new_title).lower()) and str(parent_artist).lower() == str(new_artist).lower()
(str(parent_artist).lower() == str(new_artist).lower())
): ):
name_and_tags.append( name_and_tags.append(
f" {new_fname=}, {parent_title}{new_title}, " f" {new_fname=}, {parent_title}{new_title}, "
f" {parent_artist}{new_artist}" f" {parent_artist}{new_artist}"
) )
if process_name_and_tags_matches: if process_name_and_tags_matches:
process_track(new_path, parent_path, new_title, process_track(new_path, parent_path, new_title, new_artist, bitrate)
new_artist, bitrate)
continue continue
# Check for matching tags although filename is different # Check for matching tags although filename is different
if new_title.lower() in titles_to_path: if new_title.lower() in titles_to_path:
possible_path = titles_to_path[new_title.lower()] possible_path = titles_to_path[new_title.lower()]
if parents[possible_path]['artist'].lower() == new_artist.lower(): if parents[possible_path]["artist"].lower() == new_artist.lower():
# print( # print(
# f"title={new_title}, artist={new_artist}:\n" # f"title={new_title}, artist={new_artist}:\n"
# f" {new_path} → {parent_path}" # f" {new_path} → {parent_path}"
# ) # )
tags_not_name.append( tags_not_name.append(
f"title={new_title}, artist={new_artist}:\n" f"title={new_title}, artist={new_artist}:\n"
f" {new_path}{parent_path}" f" {new_path}{parent_path}"
) )
if process_tag_matches: if process_tag_matches:
process_track(new_path, possible_path, new_title, process_track(
new_artist, bitrate) new_path, possible_path, new_title, new_artist, bitrate
)
continue continue
else: else:
no_match += 1 no_match += 1
@ -132,8 +127,8 @@ def main():
if choice: if choice:
old_file = os.path.join(parent_dir, choice[0]) old_file = os.path.join(parent_dir, choice[0])
oldtags = get_tags(old_file) oldtags = get_tags(old_file)
old_title = oldtags['title'] old_title = oldtags["title"]
old_artist = oldtags['artist'] old_artist = oldtags["artist"]
print() print()
print(f" File name will change {choice[0]}") print(f" File name will change {choice[0]}")
print(f"{new_fname}") print(f"{new_fname}")
@ -232,11 +227,8 @@ def main():
def process_track(src, dst, title, artist, bitrate): def process_track(src, dst, title, artist, bitrate):
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src)) new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
print( print(f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n")
f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n"
)
if not do_processing: if not do_processing:
return return

View File

@ -4,13 +4,7 @@ import os
from config import Config from config import Config
from helpers import ( from helpers import (
fade_point,
get_audio_segment,
get_tags, get_tags,
leading_silence,
normalise_track,
set_track_metadata,
trailing_silence,
) )
from log import log from log import log
from models import Tracks from models import Tracks
@ -72,12 +66,14 @@ def check_db(session):
print("Invalid paths in database") print("Invalid paths in database")
print("-------------------------") print("-------------------------")
for t in paths_not_found: for t in paths_not_found:
print(f""" print(
f"""
Track ID: {t.id} Track ID: {t.id}
Path: {t.path} Path: {t.path}
Title: {t.title} Title: {t.title}
Artist: {t.artist} Artist: {t.artist}
""") """
)
if more_files_to_report: if more_files_to_report:
print("There were more paths than listed that were not found") print("There were more paths than listed that were not found")

View File

@ -1,15 +1,15 @@
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629 # https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
import pytest import pytest
import sys
sys.path.append("app") # Flake8 doesn't like the sys.append within imports
import models # import sys
# sys.path.append("app")
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm import scoped_session, sessionmaker
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def connection(): def connection():
engine = create_engine( engine = create_engine(
@ -21,7 +21,8 @@ def connection():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def setup_database(connection): def setup_database(connection):
from app.models import Base # noqa E402 from app.models import Base # noqa E402
Base.metadata.bind = connection Base.metadata.bind = connection
Base.metadata.create_all() Base.metadata.create_all()
# seed_database() # seed_database()

1
devnotes.txt Normal file
View File

@ -0,0 +1 @@
Run Flake8 and Black

Binary file not shown.

Binary file not shown.

View File

@ -29,12 +29,10 @@ Features
* Database backed * Database backed
* Can be almost entirely keyboard driven * Can be almost entirely keyboard driven
* Playlist management * Open multiple playlists in tabs
* Easily add new tracks to playlists
* Show multiple playlists on tabs
* Play tracks from any playlist * Play tracks from any playlist
* Add notes/comments to tracks on playlist * Add notes/comments to tracks on playlist
* Automataic olour-coding of notes/comments according to content * Automatatic colour-coding of notes/comments according to content
* Preview tracks before playing to audience * Preview tracks before playing to audience
* Time of day clock * Time of day clock
* Elapsed track time counter * Elapsed track time counter
@ -42,8 +40,8 @@ Features
* Time to run until track is silent * Time to run until track is silent
* Graphic of volume from 5 seconds (configurable) before fade until * Graphic of volume from 5 seconds (configurable) before fade until
track is silent track is silent
* Ability to hide played tracks in playlist * Optionally hide played tracks in playlist
* Buttone to drop playout volume by 3dB for talkover * Button to drop playout volume by 3dB for talkover
* Playlist displays: * Playlist displays:
* Title * Title
* Artist * Artist
@ -51,18 +49,18 @@ Features
* Estimated start time of track * Estimated start time of track
* Estimated end time of track * Estimated end time of track
* When track was last played * When track was last played
* Bits per second (bps bitrate) of track * Bits per second (bitrate) of track
* Length of silence in recording before music starts * Length of leading silence in recording before track starts
* Total track length of arbitrary sections of tracks * Total track length of arbitrary sections of tracks
* Commands that are sent to OBS Studio (eg, for automated scene * Commands that are sent to OBS Studio (eg, for automated scene
changes) changes)
* Playlist templates * Playlist templates
* Move selected/unplayed tracks between playlists * Move selected or unplayed tracks between playlists
* Down CSV of played tracks between arbitrary dates/times * Download CSV of tracks played between arbitrary dates/times
* Search for tracks by title or artist * Search for tracks by title or artist
* Automatic search of current/next track in Wikipedia * Automatic search of current and next track in Wikipedia
* Optional search of selected track in Wikipedia * Optional search of any track in Wikipedia
* Optional search of selected track in Songfacts * Optional search of any track in Songfacts
Requirements Requirements

View File

@ -221,12 +221,10 @@ production of live internet radio shows.</p>
<ul class="simple"> <ul class="simple">
<li><p>Database backed</p></li> <li><p>Database backed</p></li>
<li><p>Can be almost entirely keyboard driven</p></li> <li><p>Can be almost entirely keyboard driven</p></li>
<li><p>Playlist management</p></li> <li><p>Open multiple playlists in tabs</p></li>
<li><p>Easily add new tracks to playlists</p></li>
<li><p>Show multiple playlists on tabs</p></li>
<li><p>Play tracks from any playlist</p></li> <li><p>Play tracks from any playlist</p></li>
<li><p>Add notes/comments to tracks on playlist</p></li> <li><p>Add notes/comments to tracks on playlist</p></li>
<li><p>Automataic olour-coding of notes/comments according to content</p></li> <li><p>Automatatic colour-coding of notes/comments according to content</p></li>
<li><p>Preview tracks before playing to audience</p></li> <li><p>Preview tracks before playing to audience</p></li>
<li><p>Time of day clock</p></li> <li><p>Time of day clock</p></li>
<li><p>Elapsed track time counter</p></li> <li><p>Elapsed track time counter</p></li>
@ -234,8 +232,8 @@ production of live internet radio shows.</p>
<li><p>Time to run until track is silent</p></li> <li><p>Time to run until track is silent</p></li>
<li><p>Graphic of volume from 5 seconds (configurable) before fade until <li><p>Graphic of volume from 5 seconds (configurable) before fade until
track is silent</p></li> track is silent</p></li>
<li><p>Ability to hide played tracks in playlist</p></li> <li><p>Optionally hide played tracks in playlist</p></li>
<li><p>Buttone to drop playout volume by 3dB for talkover</p></li> <li><p>Button to drop playout volume by 3dB for talkover</p></li>
<li><dl class="simple"> <li><dl class="simple">
<dt>Playlist displays:</dt><dd><ul> <dt>Playlist displays:</dt><dd><ul>
<li><p>Title</p></li> <li><p>Title</p></li>
@ -244,8 +242,8 @@ track is silent</p></li>
<li><p>Estimated start time of track</p></li> <li><p>Estimated start time of track</p></li>
<li><p>Estimated end time of track</p></li> <li><p>Estimated end time of track</p></li>
<li><p>When track was last played</p></li> <li><p>When track was last played</p></li>
<li><p>Bits per second (bps bitrate) of track</p></li> <li><p>Bits per second (bitrate) of track</p></li>
<li><p>Length of silence in recording before music starts</p></li> <li><p>Length of leading silence in recording before track starts</p></li>
<li><p>Total track length of arbitrary sections of tracks</p></li> <li><p>Total track length of arbitrary sections of tracks</p></li>
<li><p>Commands that are sent to OBS Studio (eg, for automated scene <li><p>Commands that are sent to OBS Studio (eg, for automated scene
changes)</p></li> changes)</p></li>
@ -254,12 +252,12 @@ changes)</p></li>
</dl> </dl>
</li> </li>
<li><p>Playlist templates</p></li> <li><p>Playlist templates</p></li>
<li><p>Move selected/unplayed tracks between playlists</p></li> <li><p>Move selected or unplayed tracks between playlists</p></li>
<li><p>Down CSV of played tracks between arbitrary dates/times</p></li> <li><p>Download CSV of tracks played between arbitrary dates/times</p></li>
<li><p>Search for tracks by title or artist</p></li> <li><p>Search for tracks by title or artist</p></li>
<li><p>Automatic search of current/next track in Wikipedia</p></li> <li><p>Automatic search of current and next track in Wikipedia</p></li>
<li><p>Optional search of selected track in Wikipedia</p></li> <li><p>Optional search of any track in Wikipedia</p></li>
<li><p>Optional search of selected track in Songfacts</p></li> <li><p>Optional search of any track in Songfacts</p></li>
</ul> </ul>
</section> </section>
<section id="requirements"> <section id="requirements">

View File

@ -1 +1 @@
Search.setIndex({"docnames": ["development", "index", "installation", "introduction", "reference", "tutorial"], "filenames": ["development.rst", "index.rst", "installation.rst", "introduction.rst", "reference.rst", "tutorial.rst"], "titles": ["Development", "Welcome to MusicMuster\u2019s documentation!", "Installation", "Introduction", "Reference", "Tutorial"], "terms": {"index": 1, "modul": 1, "search": [1, 3], "page": [1, 3], "i": 1, "music": [1, 3], "player": [1, 3], "target": 1, "product": [1, 3], "live": [1, 3], "internet": [1, 3], "radio": [1, 3], "show": [1, 3], "content": 3, "introduct": 1, "instal": [1, 3], "tutori": 1, "refer": 1, "develop": 1, "why": 1, "what": 1, "featur": 1, "requir": 1, "feedback": 1, "bug": 1, "etc": 1, "In": 3, "januari": 3, "2022": 3, "start": 3, "my": 3, "mixcloud": 3, "http": 3, "www": 3, "com": 3, "keithsmusicbox": 3, "until": 3, "had": 3, "been": 3, "an": 3, "station": 3, "which": 3, "me": 3, "us": 3, "window": 3, "playout": 3, "system": 3, "As": 3, "onli": 3, "linux": 3, "set": 3, "up": 3, "pc": 3, "specif": 3, "purpos": 3, "The": 3, "felt": 3, "were": 3, "shortcom": 3, "variou": 3, "area": 3, "onc": 3, "move": 3, "equival": 3, "didn": 3, "t": 3, "have": 3, "same": 3, "wa": 3, "unabl": 3, "find": 3, "one": 3, "met": 3, "criteria": 3, "decid": 3, "see": 3, "how": 3, "practic": 3, "would": 3, "write": 3, "own": 3, "born": 3, "It": 3, "base": 3, "whilst": 3, "could": 3, "gener": 3, "home": 3, "ar": 3, "much": 3, "better": 3, "applic": 3, "role": 3, "ha": 3, "design": 3, "support": 3, "databas": 3, "back": 3, "can": 3, "almost": 3, "entir": 3, "keyboard": 3, "driven": 3, "playlist": 3, "manag": 3, "easili": 3, "add": 3, "new": 3, "track": 3, "multipl": 3, "tab": 3, "plai": 3, "from": 3, "ani": 3, "note": 3, "comment": 3, "automata": 3, "olour": 3, "code": 3, "accord": 3, "preview": 3, "befor": 3, "audienc": 3, "time": 3, "dai": 3, "clock": 3, "elaps": 3, "counter": 3, "run": 3, "fade": 3, "silent": 3, "graphic": 3, "volum": 3, "5": 3, "second": 3, "configur": 3, "abil": 3, "hide": 3, "button": 3, "drop": 3, "3db": 3, "talkov": 3, "titl": 3, "artist": 3, "length": 3, "mm": 3, "ss": 3, "estim": 3, "end": 3, "when": 3, "last": 3, "bit": 3, "per": 3, "bp": 3, "bitrat": 3, "silenc": 3, "record": 3, "total": 3, "arbitrari": 3, "section": 3, "command": 3, "sent": 3, "ob": 3, "studio": 3, "eg": 3, "autom": 3, "scene": 3, "chang": 3, "templat": 3, "select": 3, "unplai": 3, "between": 3, "down": 3, "csv": 3, "date": 3, "automat": 3, "current": 3, "next": 3, "wikipedia": 3, "option": 3, "songfact": 3, "displai": 3, "test": 3, "debian": 3, "12": 3, "bookworm": 3, "howev": 3, "should": 3, "most": 3, "contemporari": 3, "explain": 3, "build": 3, "its": 3, "environ": 3, "automatc": 3, "all": 3, "except": 3, "version": 3, "mariadb": 3, "10": 3, "11": 3, "recent": 3, "suffic": 3, "python": 3, "3": 3, "8": 3, "later": 3, "pleas": 3, "send": 3, "keith": 3, "midnighthax": 3, "edmund": 3, "juli": 3, "2023": 3}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"welcom": 1, "musicmust": [1, 3], "": 1, "document": 1, "indic": 1, "tabl": 1, "content": 1, "develop": 0, "instal": 2, "introduct": 3, "refer": 4, "tutori": 5, "why": 3, "what": 3, "featur": 3, "requir": 3, "feedback": 3, "bug": 3, "etc": 3, "i": 3}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Welcome to MusicMuster\u2019s documentation!": [[1, "welcome-to-musicmuster-s-documentation"]], "Contents": [[1, "contents"]], "Indices and tables": [[1, "indices-and-tables"]], "Development": [[0, "development"]], "Installation": [[2, "installation"]], "Reference": [[4, "reference"]], "Tutorial": [[5, "tutorial"]], "Introduction": [[3, "introduction"]], "Why MusicMuster?": [[3, "why-musicmuster"]], "What is MusicMuster?": [[3, "what-is-musicmuster"]], "Features": [[3, "features"]], "Requirements": [[3, "requirements"]], "Feedback, bugs, etc": [[3, "feedback-bugs-etc"]]}, "indexentries": {}}) Search.setIndex({"docnames": ["development", "index", "installation", "introduction", "reference", "tutorial"], "filenames": ["development.rst", "index.rst", "installation.rst", "introduction.rst", "reference.rst", "tutorial.rst"], "titles": ["Development", "Welcome to MusicMuster\u2019s documentation!", "Installation", "Introduction", "Reference", "Tutorial"], "terms": {"index": 1, "modul": 1, "search": [1, 3], "page": [1, 3], "i": 1, "music": [1, 3], "player": [1, 3], "target": 1, "product": [1, 3], "live": [1, 3], "internet": [1, 3], "radio": [1, 3], "show": [1, 3], "content": 3, "introduct": 1, "instal": [1, 3], "tutori": 1, "refer": 1, "develop": 1, "why": 1, "what": 1, "featur": 1, "requir": 1, "feedback": 1, "bug": 1, "etc": 1, "In": 3, "januari": 3, "2022": 3, "start": 3, "my": 3, "mixcloud": 3, "http": 3, "www": 3, "com": 3, "keithsmusicbox": 3, "until": 3, "had": 3, "been": 3, "an": 3, "station": 3, "which": 3, "me": 3, "us": 3, "window": 3, "playout": 3, "system": 3, "As": 3, "onli": 3, "linux": 3, "set": 3, "up": 3, "pc": 3, "specif": 3, "purpos": 3, "The": 3, "felt": 3, "were": 3, "shortcom": 3, "variou": 3, "area": 3, "onc": 3, "move": 3, "equival": 3, "didn": 3, "t": 3, "have": 3, "same": 3, "wa": 3, "unabl": 3, "find": 3, "one": 3, "met": 3, "criteria": 3, "decid": 3, "see": 3, "how": 3, "practic": 3, "would": 3, "write": 3, "own": 3, "born": 3, "It": 3, "base": 3, "whilst": 3, "could": 3, "gener": 3, "home": 3, "ar": 3, "much": 3, "better": 3, "applic": 3, "role": 3, "ha": 3, "design": 3, "support": 3, "databas": 3, "back": 3, "can": 3, "almost": 3, "entir": 3, "keyboard": 3, "driven": 3, "playlist": 3, "manag": [], "easili": [], "add": 3, "new": [], "track": 3, "multipl": 3, "tab": 3, "plai": 3, "from": 3, "ani": 3, "note": 3, "comment": 3, "automata": [], "olour": [], "code": 3, "accord": 3, "preview": 3, "befor": 3, "audienc": 3, "time": 3, "dai": 3, "clock": 3, "elaps": 3, "counter": 3, "run": 3, "fade": 3, "silent": 3, "graphic": 3, "volum": 3, "5": 3, "second": 3, "configur": 3, "abil": [], "hide": 3, "button": 3, "drop": 3, "3db": 3, "talkov": 3, "titl": 3, "artist": 3, "length": 3, "mm": 3, "ss": 3, "estim": 3, "end": 3, "when": 3, "last": 3, "bit": 3, "per": 3, "bp": [], "bitrat": 3, "silenc": 3, "record": 3, "total": 3, "arbitrari": 3, "section": 3, "command": 3, "sent": 3, "ob": 3, "studio": 3, "eg": 3, "autom": 3, "scene": 3, "chang": 3, "templat": 3, "select": 3, "unplai": 3, "between": 3, "down": [], "csv": 3, "date": 3, "automat": 3, "current": 3, "next": 3, "wikipedia": 3, "option": 3, "songfact": 3, "displai": 3, "test": 3, "debian": 3, "12": 3, "bookworm": 3, "howev": 3, "should": 3, "most": 3, "contemporari": 3, "explain": 3, "build": 3, "its": 3, "environ": 3, "automatc": 3, "all": 3, "except": 3, "version": 3, "mariadb": 3, "10": 3, "11": 3, "recent": 3, "suffic": 3, "python": 3, "3": 3, "8": 3, "later": 3, "pleas": 3, "send": 3, "keith": 3, "midnighthax": 3, "edmund": 3, "juli": 3, "2023": 3, "open": 3, "automatat": 3, "colour": 3, "lead": 3, "download": 3}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"welcom": 1, "musicmust": [1, 3], "": 1, "document": 1, "indic": 1, "tabl": 1, "content": 1, "develop": 0, "instal": 2, "introduct": 3, "refer": 4, "tutori": 5, "why": 3, "what": 3, "featur": 3, "requir": 3, "feedback": 3, "bug": 3, "etc": 3, "i": 3}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Welcome to MusicMuster\u2019s documentation!": [[1, "welcome-to-musicmuster-s-documentation"]], "Contents": [[1, "contents"]], "Indices and tables": [[1, "indices-and-tables"]], "Development": [[0, "development"]], "Installation": [[2, "installation"]], "Reference": [[4, "reference"]], "Tutorial": [[5, "tutorial"]], "Introduction": [[3, "introduction"]], "Why MusicMuster?": [[3, "why-musicmuster"]], "What is MusicMuster?": [[3, "what-is-musicmuster"]], "Features": [[3, "features"]], "Requirements": [[3, "requirements"]], "Feedback, bugs, etc": [[3, "feedback-bugs-etc"]]}, "indexentries": {}})

View File

@ -29,11 +29,10 @@ Features
* Database backed * Database backed
* Can be almost entirely keyboard driven * Can be almost entirely keyboard driven
* Easily add new tracks to playlists * Open multiple playlists in tabs
* Show multiple playlists on tabs
* Play tracks from any playlist * Play tracks from any playlist
* Add notes/comments to tracks on playlist * Add notes/comments to tracks on playlist
* Automataic olour-coding of notes/comments according to content * Automatatic colour-coding of notes/comments according to content
* Preview tracks before playing to audience * Preview tracks before playing to audience
* Time of day clock * Time of day clock
* Elapsed track time counter * Elapsed track time counter
@ -41,8 +40,8 @@ Features
* Time to run until track is silent * Time to run until track is silent
* Graphic of volume from 5 seconds (configurable) before fade until * Graphic of volume from 5 seconds (configurable) before fade until
track is silent track is silent
* Ability to hide played tracks in playlist * Optionally hide played tracks in playlist
* Buttone to drop playout volume by 3dB for talkover * Button to drop playout volume by 3dB for talkover
* Playlist displays: * Playlist displays:
* Title * Title
* Artist * Artist
@ -50,18 +49,18 @@ Features
* Estimated start time of track * Estimated start time of track
* Estimated end time of track * Estimated end time of track
* When track was last played * When track was last played
* Bits per second (bps bitrate) of track * Bits per second (bitrate) of track
* Length of silence in recording before music starts * Length of leading silence in recording before track starts
* Total track length of arbitrary sections of tracks * Total track length of arbitrary sections of tracks
* Commands that are sent to OBS Studio (eg, for automated scene * Commands that are sent to OBS Studio (eg, for automated scene
changes) changes)
* Playlist templates * Playlist templates
* Move selected/unplayed tracks between playlists * Move selected or unplayed tracks between playlists
* Down CSV of played tracks between arbitrary dates/times * Download CSV of tracks played between arbitrary dates/times
* Search for tracks by title or artist * Search for tracks by title or artist
* Automatic search of current/next track in Wikipedia * Automatic search of current and next track in Wikipedia
* Optional search of selected track in Wikipedia * Optional search of any track in Wikipedia
* Optional search of selected track in Songfacts * Optional search of any track in Songfacts
Requirements Requirements

802
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,8 @@ mypy = "^0.991"
pudb = "^2022.1.3" pudb = "^2022.1.3"
sphinx = "^7.0.1" sphinx = "^7.0.1"
furo = "^2023.5.20" furo = "^2023.5.20"
black = "^23.3.0"
flakehell = "^0.9.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@ -2,6 +2,7 @@
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtGui, QtWidgets
class TabBar(QtWidgets.QTabBar): class TabBar(QtWidgets.QTabBar):
def paintEvent(self, event): def paintEvent(self, event):
painter = QtWidgets.QStylePainter(self) painter = QtWidgets.QStylePainter(self)
@ -13,16 +14,18 @@ class TabBar(QtWidgets.QTabBar):
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
class Window(QtWidgets.QTabWidget): class Window(QtWidgets.QTabWidget):
def __init__(self): def __init__(self):
QtWidgets.QTabWidget.__init__(self) QtWidgets.QTabWidget.__init__(self)
self.setTabBar(TabBar(self)) self.setTabBar(TabBar(self))
for color in 'tomato orange yellow lightgreen skyblue plum'.split(): for color in "tomato orange yellow lightgreen skyblue plum".split():
self.addTab(QtWidgets.QWidget(self), color) self.addTab(QtWidgets.QWidget(self), color)
if __name__ == '__main__':
if __name__ == "__main__":
import sys import sys
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
window = Window() window = Window()
window.resize(420, 200) window.resize(420, 200)

View File

@ -1,7 +1,12 @@
from config import Config
from datetime import datetime, timedelta from datetime import datetime, timedelta
from helpers import * from helpers import (
from models import Tracks fade_point,
get_audio_segment,
get_tags,
get_relative_date,
leading_silence,
ms_to_mmss,
)
def test_fade_point(): def test_fade_point():
@ -18,8 +23,8 @@ def test_fade_point():
testdata = eval(f.read()) testdata = eval(f.read())
# Volume detection can vary, so ± 1 second is OK # Volume detection can vary, so ± 1 second is OK
assert fade_at < testdata['fade_at'] + 1000 assert fade_at < testdata["fade_at"] + 1000
assert fade_at > testdata['fade_at'] - 1000 assert fade_at > testdata["fade_at"] - 1000
def test_get_tags(): def test_get_tags():
@ -32,8 +37,8 @@ def test_get_tags():
with open(test_track_data) as f: with open(test_track_data) as f:
testdata = eval(f.read()) testdata = eval(f.read())
assert tags['artist'] == testdata['artist'] assert tags["artist"] == testdata["artist"]
assert tags['title'] == testdata['title'] assert tags["title"] == testdata["title"]
def test_get_relative_date(): def test_get_relative_date():
@ -44,8 +49,7 @@ def test_get_relative_date():
eight_days_ago = today_at_10 - timedelta(days=8) eight_days_ago = today_at_10 - timedelta(days=8)
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago" assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
sixteen_days_ago = today_at_10 - timedelta(days=16) sixteen_days_ago = today_at_10 - timedelta(days=16)
assert get_relative_date( assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
def test_leading_silence(): def test_leading_silence():
@ -62,8 +66,8 @@ def test_leading_silence():
testdata = eval(f.read()) testdata = eval(f.read())
# Volume detection can vary, so ± 1 second is OK # Volume detection can vary, so ± 1 second is OK
assert silence_at < testdata['leading_silence'] + 1000 assert silence_at < testdata["leading_silence"] + 1000
assert silence_at > testdata['leading_silence'] - 1000 assert silence_at > testdata["leading_silence"] - 1000
def test_ms_to_mmss(): def test_ms_to_mmss():

View File

@ -135,7 +135,6 @@ def test_playdates_remove_track(session):
track_path = "/a/b/c" track_path = "/a/b/c"
track = Tracks(session, track_path) track = Tracks(session, track_path)
playdate = Playdates(session, track.id)
Playdates.remove_track(session, track.id) Playdates.remove_track(session, track.id)
last_played = Playdates.last_played(session, track.id) last_played = Playdates.last_played(session, track.id)
@ -149,10 +148,8 @@ def test_playlist_create(session):
def test_playlist_add_note(session): def test_playlist_add_note(session):
note_text = "my note" note_text = "my note"
note_row = 2
playlist = Playlists(session, "my playlist") playlist = Playlists(session, "my playlist")
note = playlist.add_note(session, note_row, note_text)
assert len(playlist.notes) == 1 assert len(playlist.notes) == 1
playlist_note = playlist.notes[0] playlist_note = playlist.notes[0]
@ -356,9 +353,7 @@ def test_tracks_get_all_paths(session):
def test_tracks_get_all_tracks(session): def test_tracks_get_all_tracks(session):
# Need two tracks # Need two tracks
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track2_path = "/m/n/o" track2_path = "/m/n/o"
track2 = Tracks(session, track2_path)
result = Tracks.get_all_tracks(session) result = Tracks.get_all_tracks(session)
assert track1_path in [a.path for a in result] assert track1_path in [a.path for a in result]
@ -369,9 +364,7 @@ def test_tracks_by_filename(session):
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1 = Tracks(session, track1_path) track1 = Tracks(session, track1_path)
assert Tracks.get_by_filename( assert Tracks.get_by_filename(session, os.path.basename(track1_path)) is track1
session, os.path.basename(track1_path)
) is track1
def test_tracks_by_path(session): def test_tracks_by_path(session):
@ -403,26 +396,24 @@ def test_tracks_rescan(session):
# Re-read the track # Re-read the track
track_read = Tracks.get_by_path(session, test_track_path) track_read = Tracks.get_by_path(session, test_track_path)
assert track_read.duration == testdata['duration'] assert track_read.duration == testdata["duration"]
assert track_read.start_gap == testdata['leading_silence'] assert track_read.start_gap == testdata["leading_silence"]
# Silence detection can vary, so ± 1 second is OK # Silence detection can vary, so ± 1 second is OK
assert track_read.fade_at < testdata['fade_at'] + 1000 assert track_read.fade_at < testdata["fade_at"] + 1000
assert track_read.fade_at > testdata['fade_at'] - 1000 assert track_read.fade_at > testdata["fade_at"] - 1000
assert track_read.silence_at < testdata['trailing_silence'] + 1000 assert track_read.silence_at < testdata["trailing_silence"] + 1000
assert track_read.silence_at > testdata['trailing_silence'] - 1000 assert track_read.silence_at > testdata["trailing_silence"] - 1000
def test_tracks_remove_by_path(session): def test_tracks_remove_by_path(session):
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
assert len(Tracks.get_all_tracks(session)) == 1 assert len(Tracks.get_all_tracks(session)) == 1
Tracks.remove_by_path(session, track1_path) Tracks.remove_by_path(session, track1_path)
assert len(Tracks.get_all_tracks(session)) == 0 assert len(Tracks.get_all_tracks(session)) == 0
def test_tracks_search_artists(session): def test_tracks_search_artists(session):
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1_artist = "Artist One" track1_artist = "Artist One"
track1 = Tracks(session, track1_path) track1 = Tracks(session, track1_path)
@ -435,7 +426,6 @@ def test_tracks_search_artists(session):
session.commit() session.commit()
x = Tracks.get_all_tracks(session)
artist_first_word = track1_artist.split()[0].lower() artist_first_word = track1_artist.split()[0].lower()
assert len(Tracks.search_artists(session, artist_first_word)) == 2 assert len(Tracks.search_artists(session, artist_first_word)) == 2
assert len(Tracks.search_artists(session, track1_artist)) == 1 assert len(Tracks.search_artists(session, track1_artist)) == 1
@ -454,7 +444,6 @@ def test_tracks_search_titles(session):
session.commit() session.commit()
x = Tracks.get_all_tracks(session)
title_first_word = track1_title.split()[0].lower() title_first_word = track1_title.split()[0].lower()
assert len(Tracks.search_titles(session, title_first_word)) == 2 assert len(Tracks.search_titles(session, title_first_word)) == 2
assert len(Tracks.search_titles(session, track1_title)) == 1 assert len(Tracks.search_titles(session, track1_title)) == 1

View File

@ -3,7 +3,6 @@ from PyQt5.QtCore import Qt
from app import playlists from app import playlists
from app import models from app import models
from app import musicmuster from app import musicmuster
from app import dbconfig
def seed2tracks(session): def seed2tracks(session):
@ -78,7 +77,6 @@ def test_save_and_restore(qtbot, session):
def test_meta_all_clear(qtbot, session): def test_meta_all_clear(qtbot, session):
# Create playlist # Create playlist
playlist = models.Playlists(session, "my playlist") playlist = models.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id) playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
@ -107,7 +105,6 @@ def test_meta_all_clear(qtbot, session):
def test_meta(qtbot, session): def test_meta(qtbot, session):
# Create playlist # Create playlist
playlist = playlists.Playlists(session, "my playlist") playlist = playlists.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id) playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
@ -141,7 +138,7 @@ def test_meta(qtbot, session):
# Add a note # Add a note
note_text = "my note" note_text = "my note"
note_row = 7 # will be added as row 3 note_row = 7 # will be added as row 3
note = models.Notes(session, playlist.id, note_row, note_text) note = models.Notes(session, playlist.id, note_row, note_text)
playlist_tab._insert_note(session, note) playlist_tab._insert_note(session, note)
@ -211,7 +208,6 @@ def test_clear_next(qtbot, session):
def test_get_selected_row(qtbot, monkeypatch, session): def test_get_selected_row(qtbot, monkeypatch, session):
monkeypatch.setattr(musicmuster, "Session", session) monkeypatch.setattr(musicmuster, "Session", session)
monkeypatch.setattr(playlists, "Session", session) monkeypatch.setattr(playlists, "Session", session)
@ -236,15 +232,12 @@ def test_get_selected_row(qtbot, monkeypatch, session):
row0_item0 = playlist_tab.item(0, 0) row0_item0 = playlist_tab.item(0, 0)
assert row0_item0 is not None assert row0_item0 is not None
rect = playlist_tab.visualItemRect(row0_item0) rect = playlist_tab.visualItemRect(row0_item0)
qtbot.mouseClick( qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
)
row_number = playlist_tab.get_selected_row() row_number = playlist_tab.get_selected_row()
assert row_number == 0 assert row_number == 0
def test_set_next(qtbot, monkeypatch, session): def test_set_next(qtbot, monkeypatch, session):
monkeypatch.setattr(musicmuster, "Session", session) monkeypatch.setattr(musicmuster, "Session", session)
monkeypatch.setattr(playlists, "Session", session) monkeypatch.setattr(playlists, "Session", session)
seed2tracks(session) seed2tracks(session)
@ -274,14 +267,11 @@ def test_set_next(qtbot, monkeypatch, session):
row0_item2 = playlist_tab.item(0, 2) row0_item2 = playlist_tab.item(0, 2)
assert row0_item2 is not None assert row0_item2 is not None
rect = playlist_tab.visualItemRect(row0_item2) rect = playlist_tab.visualItemRect(row0_item2)
qtbot.mouseClick( qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
)
selected_title = playlist_tab.get_selected_title() selected_title = playlist_tab.get_selected_title()
assert selected_title == track1_title assert selected_title == track1_title
qtbot.keyPress(playlist_tab.viewport(), "N", qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier)
modifier=Qt.ControlModifier)
qtbot.wait(1000) qtbot.wait(1000)

24
tree.py
View File

@ -9,9 +9,10 @@ datas = {
], ],
"No Category": [ "No Category": [
("New Game", "Playnite", "", "", "Never", "Not Plated", ""), ("New Game", "Playnite", "", "", "Never", "Not Plated", ""),
] ],
} }
class GroupDelegate(QtWidgets.QStyledItemDelegate): class GroupDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None): def __init__(self, parent=None):
super(GroupDelegate, self).__init__(parent) super(GroupDelegate, self).__init__(parent)
@ -25,6 +26,7 @@ class GroupDelegate(QtWidgets.QStyledItemDelegate):
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
option.icon = self._minus_icon if is_open else self._plus_icon option.icon = self._minus_icon if is_open else self._plus_icon
class GroupView(QtWidgets.QTreeView): class GroupView(QtWidgets.QTreeView):
def __init__(self, model, parent=None): def __init__(self, model, parent=None):
super(GroupView, self).__init__(parent) super(GroupView, self).__init__(parent)
@ -48,7 +50,18 @@ class GroupModel(QtGui.QStandardItemModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(GroupModel, self).__init__(parent) super(GroupModel, self).__init__(parent)
self.setColumnCount(8) self.setColumnCount(8)
self.setHorizontalHeaderLabels(["", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""]) self.setHorizontalHeaderLabels(
[
"",
"Name",
"Library",
"Release Date",
"Genre(s)",
"Last Played",
"Time Played",
"",
]
)
for i in range(self.columnCount()): for i in range(self.columnCount()):
it = self.horizontalHeaderItem(i) it = self.horizontalHeaderItem(i)
it.setForeground(QtGui.QColor("#F2F2F2")) it.setForeground(QtGui.QColor("#F2F2F2"))
@ -84,7 +97,7 @@ class GroupModel(QtGui.QStandardItemModel):
item.setEditable(False) item.setEditable(False)
item.setBackground(QtGui.QColor("#0D1225")) item.setBackground(QtGui.QColor("#0D1225"))
item.setForeground(QtGui.QColor("#F2F2F2")) item.setForeground(QtGui.QColor("#F2F2F2"))
group_item.setChild(j, i+1, item) group_item.setChild(j, i + 1, item)
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
@ -100,11 +113,12 @@ class MainWindow(QtWidgets.QMainWindow):
for children in childrens: for children in childrens:
model.append_element_to_group(group_item, children) model.append_element_to_group(group_item, children)
if __name__ == '__main__':
if __name__ == "__main__":
import sys import sys
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
w = MainWindow() w = MainWindow()
w.resize(720, 240) w.resize(720, 240)
w.show() w.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

1
web.py
View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
from pprint import pprint
from PyQt6.QtWidgets import QApplication, QLabel from PyQt6.QtWidgets import QApplication, QLabel
from PyQt6.QtGui import QColor, QPalette from PyQt6.QtGui import QColor, QPalette