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
@ -16,7 +15,7 @@ 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,9 +80,9 @@ 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.
@ -147,7 +151,8 @@ 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

@ -2,7 +2,7 @@ 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
@ -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__(
self,
session: scoped_session,
cart_number: int,
name: Optional[str] = None, name: Optional[str] = None,
duration: Optional[int] = None, path: Optional[str] = None, duration: Optional[int] = None,
enabled: bool = True) -> 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,12 +372,13 @@ 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__(
self,
session: scoped_session, session: scoped_session,
playlist_id: int, playlist_id: int,
track_id: Optional[int], track_id: Optional[int],
row_number: int, row_number: int,
note: str = "" note: str = "",
) -> None: ) -> None:
"""Create PlaylistRows object""" """Create PlaylistRows object"""
@ -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 = (
session.execute(
select(PlaylistRows) select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id) .where(PlaylistRows.playlist_id == playlist_id)
.order_by(PlaylistRows.plr_rownum) .order_by(PlaylistRows.plr_rownum)
).scalars().all() )
.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 = (
session.execute(
select(cls) select(cls)
.where( .where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
cls.playlist_id == playlist_id, .order_by(cls.plr_rownum)
cls.id.in_(plr_ids) )
).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 = (
session.execute(
select(cls) select(cls)
.where( .where(cls.playlist_id == playlist_id, cls.played.is_(True))
cls.playlist_id == playlist_id,
cls.played.is_(True)
)
.order_by(cls.plr_rownum) .order_by(cls.plr_rownum)
).scalars().all() )
.scalars()
.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 = (
session.execute(
select(cls) select(cls)
.where( .where(
cls.playlist_id == playlist_id, cls.playlist_id == playlist_id,
cls.track_id.is_not(None), cls.track_id.is_not(None),
cls.played.is_(False) cls.played.is_(False),
) )
.order_by(cls.plr_rownum) .order_by(cls.plr_rownum)
).scalars().all() )
.scalars()
.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")
@ -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)
.where(Tracks.path == path)
).scalar_one() ).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,9 +1,9 @@
# 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
@ -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.

View File

@ -11,7 +11,6 @@ import subprocess
import sys import sys
import threading import threading
import icons_rc
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pygame import mixer from pygame import mixer
from time import sleep from time import sleep
@ -61,15 +60,7 @@ from dbconfig import (
) )
import helpers import helpers
import music import music
from models import ( from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
Base,
Carts,
Playdates,
PlaylistRows,
Playlists,
Settings,
Tracks
)
from config import Config from config import Config
from playlists import PlaylistTab from playlists import PlaylistTab
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
@ -93,7 +84,7 @@ class CartButton(QPushButton):
self.cart_id = cart.id self.cart_id = cart.id
if cart.path and cart.enabled and not cart.duration: if cart.path and cart.enabled and not cart.duration:
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
cart.duration = tags['duration'] cart.duration = tags["duration"]
self.duration = cart.duration self.duration = cart.duration
self.path = cart.path self.path = cart.path
self.player = None self.player = None
@ -110,8 +101,9 @@ class CartButton(QPushButton):
self.pgb.setTextVisible(False) self.pgb.setTextVisible(False)
self.pgb.setVisible(False) self.pgb.setVisible(False)
palette = self.pgb.palette() palette = self.pgb.palette()
palette.setColor(QPalette.ColorRole.Highlight, palette.setColor(
QColor(Config.COLOUR_CART_PROGRESSBAR)) QPalette.ColorRole.Highlight, QColor(Config.COLOUR_CART_PROGRESSBAR)
)
self.pgb.setPalette(palette) self.pgb.setPalette(palette)
self.pgb.setGeometry(0, 0, self.width(), 10) self.pgb.setGeometry(0, 0, self.width(), 10)
self.pgb.setMinimum(0) self.pgb.setMinimum(0)
@ -157,16 +149,13 @@ class FadeCurve:
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE # Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence # milliseconds before fade starts to silence
self.start_ms = max( self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track.silence_at self.end_ms = track.silence_at
self.audio_segment = audio[self.start_ms:self.end_ms] self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples()) self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array # Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = ( self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
len(self.graph_array) / (self.end_ms - self.start_ms)
)
self.region = None self.region = None
@ -192,10 +181,7 @@ class FadeCurve:
if self.region is None: if self.region is None:
# Create the region now that we're into fade # Create the region now that we're into fade
self.region = pg.LinearRegionItem( self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
[0, 0],
bounds=[0, len(self.graph_array)]
)
self.GraphWidget.addItem(self.region) self.GraphWidget.addItem(self.region)
# Update region position # Update region position
@ -286,8 +272,9 @@ class PlaylistTrack:
f"playlist_id={self.playlist_id}>" f"playlist_id={self.playlist_id}>"
) )
def set_plr(self, session: scoped_session, plr: PlaylistRows, def set_plr(
tab: PlaylistTab) -> None: self, session: scoped_session, plr: PlaylistRows, tab: PlaylistTab
) -> None:
""" """
Update with new plr information Update with new plr information
""" """
@ -323,8 +310,7 @@ class PlaylistTrack:
self.start_time = datetime.now() self.start_time = datetime.now()
if self.duration: if self.duration:
self.end_time = ( self.end_time = self.start_time + timedelta(milliseconds=self.duration)
self.start_time + timedelta(milliseconds=self.duration))
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
@ -355,14 +341,15 @@ class Window(QMainWindow, Ui_MainWindow):
self.txtSearch.setHidden(True) self.txtSearch.setHidden(True)
self.hide_played_tracks = False self.hide_played_tracks = False
mixer.init() mixer.init()
self.widgetFadeVolume.hideAxis('bottom') self.widgetFadeVolume.hideAxis("bottom")
self.widgetFadeVolume.hideAxis('left') self.widgetFadeVolume.hideAxis("left")
self.widgetFadeVolume.setDefaultPadding(0) self.widgetFadeVolume.setDefaultPadding(0)
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND) self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
FadeCurve.GraphWidget = self.widgetFadeVolume FadeCurve.GraphWidget = self.widgetFadeVolume
self.visible_playlist_tab: Callable[[], PlaylistTab] = \ self.visible_playlist_tab: Callable[
self.tabPlaylist.currentWidget [], PlaylistTab
] = self.tabPlaylist.currentWidget
self.load_last_playlists() self.load_last_playlists()
if Config.CARTS_HIDE: if Config.CARTS_HIDE:
@ -380,10 +367,8 @@ class Window(QMainWindow, Ui_MainWindow):
try: try:
git_tag = str( git_tag = str(
subprocess.check_output( subprocess.check_output(["git", "describe"], stderr=subprocess.STDOUT)
['git', 'describe'], stderr=subprocess.STDOUT ).strip("'b\\n")
)
).strip('\'b\\n')
except subprocess.CalledProcessError as exc_info: except subprocess.CalledProcessError as exc_info:
git_tag = str(exc_info.output) git_tag = str(exc_info.output)
@ -395,7 +380,7 @@ class Window(QMainWindow, Ui_MainWindow):
self, self,
"About", "About",
f"MusicMuster {git_tag}\n\nDatabase: {dbname}", f"MusicMuster {git_tag}\n\nDatabase: {dbname}",
QMessageBox.StandardButton.Ok QMessageBox.StandardButton.Ok,
) )
def cart_configure(self, cart: Carts, btn: CartButton) -> None: def cart_configure(self, cart: Carts, btn: CartButton) -> None:
@ -433,15 +418,13 @@ class Window(QMainWindow, Ui_MainWindow):
# Don't allow clicks while we're playing # Don't allow clicks while we're playing
btn.setEnabled(False) btn.setEnabled(False)
if not btn.player: if not btn.player:
log.debug( log.debug(f"musicmuster.cart_click(): no player assigned ({btn=})")
f"musicmuster.cart_click(): no player assigned ({btn=})")
return return
btn.player.play() btn.player.play()
btn.is_playing = True btn.is_playing = True
colour = Config.COLOUR_CART_PLAYING colour = Config.COLOUR_CART_PLAYING
thread = threading.Thread(target=self.cart_progressbar, thread = threading.Thread(target=self.cart_progressbar, args=(btn,))
args=(btn,))
thread.start() thread.start()
else: else:
colour = Config.COLOUR_CART_ERROR colour = Config.COLOUR_CART_ERROR
@ -469,7 +452,7 @@ class Window(QMainWindow, Ui_MainWindow):
return return
if cart.path and not helpers.file_is_unreadable(cart.path): if cart.path and not helpers.file_is_unreadable(cart.path):
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
cart.duration = tags['duration'] cart.duration = tags["duration"]
cart.enabled = dlg.ui.chkEnabled.isChecked() cart.enabled = dlg.ui.chkEnabled.isChecked()
cart.name = name cart.name = name
@ -488,8 +471,7 @@ class Window(QMainWindow, Ui_MainWindow):
for cart_number in range(1, Config.CARTS_COUNT + 1): for cart_number in range(1, Config.CARTS_COUNT + 1):
cart = session.query(Carts).get(cart_number) cart = session.query(Carts).get(cart_number)
if cart is None: if cart is None:
cart = Carts(session, cart_number, cart = Carts(session, cart_number, name=f"Cart #{cart_number}")
name=f"Cart #{cart_number}")
btn = CartButton(self, cart) btn = CartButton(self, cart)
btn.clicked.connect(self.cart_click) btn.clicked.connect(self.cart_click)
@ -540,7 +522,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.update_headers() self.update_headers()
def clear_selection(self) -> None: def clear_selection(self) -> None:
""" Clear selected row""" """Clear selected row"""
# Unselect any selected rows # Unselect any selected rows
if self.visible_playlist_tab(): if self.visible_playlist_tab():
@ -555,27 +537,25 @@ class Window(QMainWindow, Ui_MainWindow):
if self.playing: if self.playing:
event.ignore() event.ignore()
helpers.show_warning( helpers.show_warning(
self, self, "Track playing", "Can't close application while track is playing"
"Track playing", )
"Can't close application while track is playing")
else: else:
with Session() as session: with Session() as session:
record = Settings.get_int_settings( record = Settings.get_int_settings(session, "mainwindow_height")
session, "mainwindow_height")
if record.f_int != self.height(): if record.f_int != self.height():
record.update(session, {'f_int': self.height()}) record.update(session, {"f_int": self.height()})
record = Settings.get_int_settings(session, "mainwindow_width") record = Settings.get_int_settings(session, "mainwindow_width")
if record.f_int != self.width(): if record.f_int != self.width():
record.update(session, {'f_int': self.width()}) record.update(session, {"f_int": self.width()})
record = Settings.get_int_settings(session, "mainwindow_x") record = Settings.get_int_settings(session, "mainwindow_x")
if record.f_int != self.x(): if record.f_int != self.x():
record.update(session, {'f_int': self.x()}) record.update(session, {"f_int": self.x()})
record = Settings.get_int_settings(session, "mainwindow_y") record = Settings.get_int_settings(session, "mainwindow_y")
if record.f_int != self.y(): if record.f_int != self.y():
record.update(session, {'f_int': self.y()}) record.update(session, {"f_int": self.y()})
# Save splitter settings # Save splitter settings
splitter_sizes = self.splitter.sizes() splitter_sizes = self.splitter.sizes()
@ -584,16 +564,15 @@ class Window(QMainWindow, Ui_MainWindow):
record = Settings.get_int_settings(session, "splitter_top") record = Settings.get_int_settings(session, "splitter_top")
if record.f_int != splitter_top: if record.f_int != splitter_top:
record.update(session, {'f_int': splitter_top}) record.update(session, {"f_int": splitter_top})
record = Settings.get_int_settings(session, "splitter_bottom") record = Settings.get_int_settings(session, "splitter_bottom")
if record.f_int != splitter_bottom: if record.f_int != splitter_bottom:
record.update(session, {'f_int': splitter_bottom}) record.update(session, {"f_int": splitter_bottom})
# Save current tab # Save current tab
record = Settings.get_int_settings(session, "active_tab") record = Settings.get_int_settings(session, "active_tab")
record.update(session, record.update(session, {"f_int": self.tabPlaylist.currentIndex()})
{'f_int': self.tabPlaylist.currentIndex()})
event.accept() event.accept()
@ -613,10 +592,8 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Don't close current track playlist # Don't close current track playlist
if self.tabPlaylist.widget(tab_index) == ( if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
self.current_track.playlist_tab): self.statusbar.showMessage("Can't close current track playlist", 5000)
self.statusbar.showMessage(
"Can't close current track playlist", 5000)
return False return False
# Attempt to close next track playlist # Attempt to close next track playlist
@ -643,15 +620,17 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
self.actionDeletePlaylist.triggered.connect(self.delete_playlist) self.actionDeletePlaylist.triggered.connect(self.delete_playlist)
self.actionDownload_CSV_of_played_tracks.triggered.connect( self.actionDownload_CSV_of_played_tracks.triggered.connect(
self.download_played_tracks) self.download_played_tracks
self.actionEnable_controls.triggered.connect( )
self.enable_play_next_controls) self.actionEnable_controls.triggered.connect(self.enable_play_next_controls)
self.actionExport_playlist.triggered.connect(self.export_playlist_tab) self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
self.actionFade.triggered.connect(self.fade) self.actionFade.triggered.connect(self.fade)
self.actionFind_next.triggered.connect( self.actionFind_next.triggered.connect(
lambda: self.tabPlaylist.currentWidget().search_next()) lambda: self.tabPlaylist.currentWidget().search_next()
)
self.actionFind_previous.triggered.connect( self.actionFind_previous.triggered.connect(
lambda: self.tabPlaylist.currentWidget().search_previous()) lambda: self.tabPlaylist.currentWidget().search_previous()
)
self.actionImport.triggered.connect(self.import_track) self.actionImport.triggered.connect(self.import_track)
self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertSectionHeader.triggered.connect(self.insert_header)
self.actionInsertTrack.triggered.connect(self.insert_track) self.actionInsertTrack.triggered.connect(self.insert_track)
@ -666,13 +645,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionResume.triggered.connect(self.resume) self.actionResume.triggered.connect(self.resume)
self.actionSave_as_template.triggered.connect(self.save_as_template) self.actionSave_as_template.triggered.connect(self.save_as_template)
self.actionSearch_title_in_Songfacts.triggered.connect( self.actionSearch_title_in_Songfacts.triggered.connect(
lambda: self.tabPlaylist.currentWidget().lookup_row_in_songfacts()) lambda: self.tabPlaylist.currentWidget().lookup_row_in_songfacts()
)
self.actionSearch_title_in_Wikipedia.triggered.connect( self.actionSearch_title_in_Wikipedia.triggered.connect(
lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia()) lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia()
)
self.actionSearch.triggered.connect(self.search_playlist) self.actionSearch.triggered.connect(self.search_playlist)
self.actionSelect_next_track.triggered.connect(self.select_next_row) self.actionSelect_next_track.triggered.connect(self.select_next_row)
self.actionSelect_previous_track.triggered.connect( self.actionSelect_previous_track.triggered.connect(self.select_previous_row)
self.select_previous_row)
self.actionMoveUnplayed.triggered.connect(self.move_unplayed) self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
self.actionSetNext.triggered.connect(self.set_selected_track_next) self.actionSetNext.triggered.connect(self.set_selected_track_next)
self.actionSkipToNext.triggered.connect(self.play_next) self.actionSkipToNext.triggered.connect(self.play_next)
@ -691,10 +671,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def create_playlist(self, def create_playlist(
session: scoped_session, self, session: scoped_session, playlist_name: Optional[str] = None
playlist_name: Optional[str] = None) \ ) -> Optional[Playlists]:
-> Optional[Playlists]:
"""Create new playlist""" """Create new playlist"""
playlist_name = self.solicit_playlist_name() playlist_name = self.solicit_playlist_name()
@ -712,8 +691,7 @@ class Window(QMainWindow, Ui_MainWindow):
if playlist: if playlist:
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session: scoped_session, def create_playlist_tab(self, session: scoped_session, playlist: Playlists) -> int:
playlist: Playlists) -> int:
""" """
Take the passed playlist database object, create a playlist tab and Take the passed playlist database object, create a playlist tab and
add tab to display. Return index number of tab. add tab to display. Return index number of tab.
@ -722,8 +700,11 @@ class Window(QMainWindow, Ui_MainWindow):
assert playlist.id assert playlist.id
playlist_tab = PlaylistTab( playlist_tab = PlaylistTab(
musicmuster=self, session=session, playlist_id=playlist.id, musicmuster=self,
signals=self.signals) session=session,
playlist_id=playlist.id,
signals=self.signals,
)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
@ -737,8 +718,9 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
# Save the selected PlaylistRows items ready for a later # Save the selected PlaylistRows items ready for a later
# paste # paste
self.selected_plrs = ( self.selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
self.visible_playlist_tab().get_selected_playlistrows(session)) session
)
def debug(self): def debug(self):
"""Invoke debugger""" """Invoke debugger"""
@ -746,6 +728,7 @@ class Window(QMainWindow, Ui_MainWindow):
visible_playlist_id = self.visible_playlist_tab().playlist_id visible_playlist_id = self.visible_playlist_tab().playlist_id
print(f"Active playlist id={visible_playlist_id}") print(f"Active playlist id={visible_playlist_id}")
import ipdb # type: ignore import ipdb # type: ignore
ipdb.set_trace() ipdb.set_trace()
def delete_playlist(self) -> None: def delete_playlist(self) -> None:
@ -757,9 +740,9 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_id = self.visible_playlist_tab().playlist_id playlist_id = self.visible_playlist_tab().playlist_id
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if playlist: if playlist:
if helpers.ask_yes_no("Delete playlist", if helpers.ask_yes_no(
f"Delete playlist '{playlist.name}': " "Delete playlist",
"Are you sure?" f"Delete playlist '{playlist.name}': " "Are you sure?",
): ):
if self.close_playlist_tab(): if self.close_playlist_tab():
playlist.delete(session) playlist.delete(session)
@ -780,9 +763,10 @@ class Window(QMainWindow, Ui_MainWindow):
start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime() start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime()
# Get output filename # Get output filename
pathspec = QFileDialog.getSaveFileName( pathspec = QFileDialog.getSaveFileName(
self, 'Save CSV of tracks played', self,
"Save CSV of tracks played",
directory="/tmp/playlist.csv", directory="/tmp/playlist.csv",
filter="CSV files (*.csv)" filter="CSV files (*.csv)",
) )
if not pathspec: if not pathspec:
return return
@ -794,9 +778,7 @@ class Window(QMainWindow, Ui_MainWindow):
with open(path, "w") as f: with open(path, "w") as f:
with Session() as session: with Session() as session:
for playdate in Playdates.played_after(session, start_dt): for playdate in Playdates.played_after(session, start_dt):
f.write( f.write(f"{playdate.track.artist},{playdate.track.title}\n")
f"{playdate.track.artist},{playdate.track.title}\n"
)
def drop3db(self) -> None: def drop3db(self) -> None:
"""Drop music level by 3db if button checked""" """Drop music level by 3db if button checked"""
@ -867,16 +849,16 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_id = self.visible_playlist_tab().playlist_id playlist_id = self.visible_playlist_tab().playlist_id
with Session() as session: with Session() as session:
# Get output filename # Get output filename
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if not playlist: if not playlist:
return return
pathspec = QFileDialog.getSaveFileName( pathspec = QFileDialog.getSaveFileName(
self, 'Save Playlist', self,
"Save Playlist",
directory=f"{playlist.name}.m3u", directory=f"{playlist.name}.m3u",
filter="M3U files (*.m3u);;All files (*.*)" filter="M3U files (*.m3u);;All files (*.*)",
) )
if not pathspec: if not pathspec:
return return
@ -923,10 +905,7 @@ class Window(QMainWindow, Ui_MainWindow):
times a second; this function has much better resolution. times a second; this function has much better resolution.
""" """
if ( if self.current_track.track_id is None or self.current_track.start_time is None:
self.current_track.track_id is None or
self.current_track.start_time is None
):
return 0 return 0
now = datetime.now() now = datetime.now()
@ -965,16 +944,16 @@ class Window(QMainWindow, Ui_MainWindow):
txt = "" txt = ""
tags = helpers.get_tags(fname) tags = helpers.get_tags(fname)
new_tracks.append(fname) new_tracks.append(fname)
title = tags['title'] title = tags["title"]
artist = tags['artist'] artist = tags["artist"]
count = 0 count = 0
possible_matches = Tracks.search_titles(session, title) possible_matches = Tracks.search_titles(session, title)
if possible_matches: if possible_matches:
txt += 'Similar to new track ' txt += "Similar to new track "
txt += f'"{title}" by "{artist} ({fname})":\n\n' txt += f'"{title}" by "{artist} ({fname})":\n\n'
for track in possible_matches: for track in possible_matches:
txt += f' "{track.title}" by {track.artist}' txt += f' "{track.title}" by {track.artist}'
txt += f' ({track.path})\n\n' txt += f" ({track.path})\n\n"
count += 1 count += 1
if count >= Config.MAX_IMPORT_MATCHES: if count >= Config.MAX_IMPORT_MATCHES:
txt += "\nThere are more similar-looking tracks" txt += "\nThere are more similar-looking tracks"
@ -987,7 +966,7 @@ class Window(QMainWindow, Ui_MainWindow):
"Possible duplicates", "Possible duplicates",
txt, txt,
QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Cancel QMessageBox.StandardButton.Cancel,
) )
if result == QMessageBox.StandardButton.Cancel: if result == QMessageBox.StandardButton.Cancel:
return return
@ -1005,9 +984,7 @@ class Window(QMainWindow, Ui_MainWindow):
self, "Import error", "Error importing " + msg self, "Import error", "Error importing " + msg
) )
) )
self.worker.importing.connect( self.worker.importing.connect(lambda msg: self.statusbar.showMessage(msg, 5000))
lambda msg: self.statusbar.showMessage(msg, 5000)
)
self.worker.finished.connect(self.import_complete) self.worker.finished.connect(self.import_complete)
self.import_thread.start() self.import_thread.start()
@ -1057,8 +1034,9 @@ class Window(QMainWindow, Ui_MainWindow):
if record and record.f_int >= 0: if record and record.f_int >= 0:
self.tabPlaylist.setCurrentIndex(record.f_int) self.tabPlaylist.setCurrentIndex(record.f_int)
def move_playlist_rows(self, session: scoped_session, def move_playlist_rows(
playlistrows: List[PlaylistRows]) -> None: self, session: scoped_session, playlistrows: List[PlaylistRows]
) -> None:
""" """
Move passed playlist rows to another playlist Move passed playlist rows to another playlist
@ -1071,14 +1049,15 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Remove current/next rows from list # Remove current/next rows from list
plrs_to_move = [plr for plr in playlistrows if plrs_to_move = [
plr.id not in plr
[self.current_track.plr_id, for plr in playlistrows
self.next_track.plr_id] if plr.id not in [self.current_track.plr_id, self.next_track.plr_id]
] ]
rows_to_delete = [plr.plr_rownum for plr in plrs_to_move rows_to_delete = [
if plr.plr_rownum is not None] plr.plr_rownum for plr in plrs_to_move if plr.plr_rownum is not None
]
if not rows_to_delete: if not rows_to_delete:
return return
@ -1100,8 +1079,7 @@ class Window(QMainWindow, Ui_MainWindow):
destination_playlist_id = dlg.playlist.id destination_playlist_id = dlg.playlist.id
# Update destination playlist in the database # Update destination playlist in the database
last_row = PlaylistRows.get_last_used_row(session, last_row = PlaylistRows.get_last_used_row(session, destination_playlist_id)
destination_playlist_id)
if last_row is not None: if last_row is not None:
next_row = last_row + 1 next_row = last_row + 1
else: else:
@ -1135,8 +1113,9 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
with Session() as session: with Session() as session:
selected_plrs = ( selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
self.visible_playlist_tab().get_selected_playlistrows(session)) session
)
if not selected_plrs: if not selected_plrs:
return return
@ -1155,11 +1134,9 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_id = self.visible_playlist_tab().playlist_id playlist_id = self.visible_playlist_tab().playlist_id
with Session() as session: with Session() as session:
unplayed_plrs = PlaylistRows.get_unplayed_rows( unplayed_plrs = PlaylistRows.get_unplayed_rows(session, playlist_id)
session, playlist_id) if helpers.ask_yes_no(
if helpers.ask_yes_no("Move tracks", "Move tracks", f"Move {len(unplayed_plrs)} tracks:" " Are you sure?"
f"Move {len(unplayed_plrs)} tracks:"
" Are you sure?"
): ):
self.move_playlist_rows(session, unplayed_plrs) self.move_playlist_rows(session, unplayed_plrs)
@ -1168,8 +1145,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
templates = Playlists.get_all_templates(session) templates = Playlists.get_all_templates(session)
dlg = SelectPlaylistDialog(self, playlists=templates, dlg = SelectPlaylistDialog(self, playlists=templates, session=session)
session=session)
dlg.exec() dlg.exec()
template = dlg.playlist template = dlg.playlist
if template: if template:
@ -1177,7 +1153,8 @@ class Window(QMainWindow, Ui_MainWindow):
if not playlist_name: if not playlist_name:
return return
playlist = Playlists.create_playlist_from_template( playlist = Playlists.create_playlist_from_template(
session, template, playlist_name) session, template, playlist_name
)
if not playlist: if not playlist:
return return
tab_index = self.create_playlist_tab(session, playlist) tab_index = self.create_playlist_tab(session, playlist)
@ -1188,8 +1165,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
playlists = Playlists.get_closed(session) playlists = Playlists.get_closed(session)
dlg = SelectPlaylistDialog(self, playlists=playlists, dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
session=session)
dlg.exec() dlg.exec()
playlist = dlg.playlist playlist = dlg.playlist
if playlist: if playlist:
@ -1217,8 +1193,9 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
# Create space in destination playlist # Create space in destination playlist
PlaylistRows.move_rows_down(session, dst_playlist_id, PlaylistRows.move_rows_down(
dst_row, len(self.selected_plrs)) session, dst_playlist_id, dst_row, len(self.selected_plrs)
)
session.commit() session.commit()
# Update plrs # Update plrs
@ -1240,7 +1217,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Update display # Update display
self.visible_playlist_tab().populate_display( self.visible_playlist_tab().populate_display(
session, dst_playlist_id, scroll_to_top=False) session, dst_playlist_id, scroll_to_top=False
)
# If source playlist is not destination playlist, fixup row # If source playlist is not destination playlist, fixup row
# numbers and update display # numbers and update display
@ -1250,13 +1228,13 @@ class Window(QMainWindow, Ui_MainWindow):
# will be re-populated when it is opened) # will be re-populated when it is opened)
source_playlist_tab = None source_playlist_tab = None
for tab in range(self.tabPlaylist.count()): for tab in range(self.tabPlaylist.count()):
if self.tabPlaylist.widget(tab).playlist_id == \ if self.tabPlaylist.widget(tab).playlist_id == src_playlist_id:
src_playlist_id:
source_playlist_tab = self.tabPlaylist.widget(tab) source_playlist_tab = self.tabPlaylist.widget(tab)
break break
if source_playlist_tab: if source_playlist_tab:
source_playlist_tab.populate_display( source_playlist_tab.populate_display(
session, src_playlist_id, scroll_to_top=False) session, src_playlist_id, scroll_to_top=False
)
# Reset so rows can't be repasted # Reset so rows can't be repasted
self.selected_plrs = None self.selected_plrs = None
@ -1305,8 +1283,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Set current track playlist_tab colour # Set current track playlist_tab colour
current_tab = self.current_track.playlist_tab current_tab = self.current_track.playlist_tab
if current_tab: if current_tab:
self.set_tab_colour( self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
current_tab, QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active # Restore volume if -3dB active
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
@ -1356,8 +1333,7 @@ class Window(QMainWindow, Ui_MainWindow):
if self.btnPreview.isChecked(): if self.btnPreview.isChecked():
# Get track path for first selected track if there is one # Get track path for first selected track if there is one
track_path = ( track_path = self.visible_playlist_tab().get_selected_row_track_path()
self.visible_playlist_tab().get_selected_row_track_path())
if not track_path: if not track_path:
# Otherwise get path to next track to play # Otherwise get path to next track to play
track_path = self.next_track.path track_path = self.next_track.path
@ -1405,7 +1381,6 @@ class Window(QMainWindow, Ui_MainWindow):
if self.current_track.track_id: if self.current_track.track_id:
playing_track = self.current_track playing_track = self.current_track
with Session() as session:
# Set next plr to be track to resume # Set next plr to be track to resume
if not self.previous_track.plr_id: if not self.previous_track.plr_id:
return return
@ -1413,8 +1388,9 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# Resume last track # Resume last track
self.set_next_plr_id(self.previous_track.plr_id, self.set_next_plr_id(
self.previous_track.playlist_tab) self.previous_track.plr_id, self.previous_track.playlist_tab
)
self.play_next(self.previous_track_position) self.play_next(self.previous_track_position)
# If a track was playing when we were called, get details to # If a track was playing when we were called, get details to
@ -1424,16 +1400,13 @@ class Window(QMainWindow, Ui_MainWindow):
return return
if not playing_track.playlist_tab: if not playing_track.playlist_tab:
return return
self.set_next_plr_id(playing_track.plr_id, self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab)
playing_track.playlist_tab)
def save_as_template(self) -> None: def save_as_template(self) -> None:
"""Save current playlist as template""" """Save current playlist as template"""
with Session() as session: with Session() as session:
template_names = [ template_names = [a.name for a in Playlists.get_all_templates(session)]
a.name for a in Playlists.get_all_templates(session)
]
while True: while True:
# Get name for new template # Get name for new template
@ -1448,13 +1421,12 @@ class Window(QMainWindow, Ui_MainWindow):
template_name = dlg.textValue() template_name = dlg.textValue()
if template_name not in template_names: if template_name not in template_names:
break break
helpers.show_warning(self, helpers.show_warning(
"Duplicate template", self, "Duplicate template", "Template name already in use"
"Template name already in use" )
Playlists.save_as_template(
session, self.visible_playlist_tab().playlist_id, template_name
) )
Playlists.save_as_template(session,
self.visible_playlist_tab().playlist_id,
template_name)
helpers.show_OK(self, "Template", "Template saved") helpers.show_OK(self, "Template", "Template saved")
def search_playlist(self) -> None: def search_playlist(self) -> None:
@ -1535,8 +1507,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab) self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
self.tabPlaylist.currentWidget().scroll_next_to_top() self.tabPlaylist.currentWidget().scroll_next_to_top()
def solicit_playlist_name(self, def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]:
default: Optional[str] = "") -> Optional[str]:
"""Get name of playlist from user""" """Get name of playlist from user"""
dlg = QInputDialog(self) dlg = QInputDialog(self)
@ -1581,11 +1552,13 @@ class Window(QMainWindow, Ui_MainWindow):
# Reset playlist_tab colour # Reset playlist_tab colour
if self.current_track.playlist_tab: if self.current_track.playlist_tab:
if self.current_track.playlist_tab == self.next_track.playlist_tab: if self.current_track.playlist_tab == self.next_track.playlist_tab:
self.set_tab_colour(self.current_track.playlist_tab, self.set_tab_colour(
QColor(Config.COLOUR_NEXT_TAB)) self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
)
else: else:
self.set_tab_colour(self.current_track.playlist_tab, self.set_tab_colour(
QColor(Config.COLOUR_NORMAL_TAB)) self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
)
# Run end-of-track actions # Run end-of-track actions
self.end_of_track_actions() self.end_of_track_actions()
@ -1612,8 +1585,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.set_next_plr_id(selected_plr_ids[0], playlist_tab) self.set_next_plr_id(selected_plr_ids[0], playlist_tab)
def set_next_plr_id(self, next_plr_id: Optional[int], def set_next_plr_id(
playlist_tab: PlaylistTab) -> None: self, next_plr_id: Optional[int], playlist_tab: PlaylistTab
) -> None:
""" """
Set passed plr_id as next track to play, or clear next track if None Set passed plr_id as next track to play, or clear next track if None
@ -1636,8 +1610,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Tell playlist tabs to update their 'next track' highlighting # Tell playlist tabs to update their 'next track' highlighting
# Args must both be ints, so use zero for no next track # Args must both be ints, so use zero for no next track
self.signals.set_next_track_signal.emit(old_next_track.plr_id, self.signals.set_next_track_signal.emit(
next_plr_id or 0) old_next_track.plr_id, next_plr_id or 0
)
# Update headers # Update headers
self.update_headers() self.update_headers()
@ -1650,12 +1625,15 @@ class Window(QMainWindow, Ui_MainWindow):
# because it isn't quick # because it isn't quick
if self.next_track.title: if self.next_track.title:
QTimer.singleShot( QTimer.singleShot(
1, lambda: self.tabInfolist.open_in_wikipedia( 1,
self.next_track.title) lambda: self.tabInfolist.open_in_wikipedia(
self.next_track.title
),
) )
def _set_next_track_playlist_tab_colours( def _set_next_track_playlist_tab_colours(
self, old_next_track: Optional[PlaylistTrack]) -> None: self, old_next_track: Optional[PlaylistTrack]
) -> None:
""" """
Set playlist tab colour for next track. self.next_track needs Set playlist tab colour for next track. self.next_track needs
to be set before calling. to be set before calling.
@ -1664,14 +1642,14 @@ class Window(QMainWindow, Ui_MainWindow):
# If the original next playlist tab isn't the same as the # If the original next playlist tab isn't the same as the
# new one or the current track, it needs its colour reset. # new one or the current track, it needs its colour reset.
if ( if (
old_next_track and old_next_track
old_next_track.playlist_tab and and old_next_track.playlist_tab
old_next_track.playlist_tab not in [ and old_next_track.playlist_tab
self.next_track.playlist_tab, not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
self.current_track.playlist_tab ):
]): self.set_tab_colour(
self.set_tab_colour(old_next_track.playlist_tab, old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
QColor(Config.COLOUR_NORMAL_TAB)) )
# If the new next playlist tab isn't the same as the # If the new next playlist tab isn't the same as the
# old one or the current track, it needs its colour set. # old one or the current track, it needs its colour set.
if old_next_track: if old_next_track:
@ -1679,14 +1657,14 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
old_tab = None old_tab = None
if ( if (
self.next_track and self.next_track
self.next_track.playlist_tab and and self.next_track.playlist_tab
self.next_track.playlist_tab not in [ and self.next_track.playlist_tab
old_tab, not in [old_tab, self.current_track.playlist_tab]
self.current_track.playlist_tab ):
]): self.set_tab_colour(
self.set_tab_colour(self.next_track.playlist_tab, self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
QColor(Config.COLOUR_NEXT_TAB)) )
def tick(self) -> None: def tick(self) -> None:
""" """
@ -1713,9 +1691,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Update volume fade curve # Update volume fade curve
if ( if (
self.current_track.track_id and self.current_track.track_id
self.current_track.fade_graph and and self.current_track.fade_graph
self.current_track.start_time and self.current_track.start_time
): ):
play_time = ( play_time = (
datetime.now() - self.current_track.start_time datetime.now() - self.current_track.start_time
@ -1727,8 +1705,7 @@ class Window(QMainWindow, Ui_MainWindow):
Called every 500ms Called every 500ms
""" """
self.lblTOD.setText(datetime.now().strftime( self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
Config.TOD_TIME_FORMAT))
# Update carts # Update carts
self.cart_tick() self.cart_tick()
@ -1748,15 +1725,19 @@ class Window(QMainWindow, Ui_MainWindow):
# player.is_playing() returning True, so assume playing if less # player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since # than Config.PLAY_SETTLE microseconds have passed since
# starting play. # starting play.
if self.music.player and self.current_track.start_time and ( if (
self.music.player.is_playing() or self.music.player
(datetime.now() - self.current_track.start_time) and self.current_track.start_time
< timedelta(microseconds=Config.PLAY_SETTLE)): and (
self.music.player.is_playing()
or (datetime.now() - self.current_track.start_time)
< timedelta(microseconds=Config.PLAY_SETTLE)
)
):
playtime = self.get_playtime() playtime = self.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime) time_to_fade = self.current_track.fade_at - playtime
time_to_silence = ( time_to_silence = self.current_track.silence_at - playtime
self.current_track.silence_at - playtime) time_to_end = self.current_track.duration - playtime
time_to_end = (self.current_track.duration - playtime)
# Elapsed time # Elapsed time
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
@ -1787,9 +1768,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_silent.setStyleSheet("") self.frame_silent.setStyleSheet("")
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
self.label_silent_timer.setText( self.label_silent_timer.setText(helpers.ms_to_mmss(time_to_silence))
helpers.ms_to_mmss(time_to_silence)
)
# Time to end # Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
@ -1834,8 +1813,9 @@ class Window(QMainWindow, Ui_MainWindow):
class CartDialog(QDialog): class CartDialog(QDialog):
"""Edit cart details""" """Edit cart details"""
def __init__(self, musicmuster: Window, session: scoped_session, def __init__(
cart: Carts, *args, **kwargs) -> None: self, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs
) -> None:
""" """
Manage carts Manage carts
""" """
@ -1871,8 +1851,14 @@ class CartDialog(QDialog):
class DbDialog(QDialog): class DbDialog(QDialog):
"""Select track from database""" """Select track from database"""
def __init__(self, musicmuster: Window, session: scoped_session, def __init__(
get_one_track: bool = False, *args, **kwargs) -> None: self,
musicmuster: Window,
session: scoped_session,
get_one_track: bool = False,
*args,
**kwargs,
) -> None:
""" """
Subclassed QDialog to manage track selection Subclassed QDialog to manage track selection
@ -1911,17 +1897,16 @@ class DbDialog(QDialog):
record = Settings.get_int_settings(self.session, "dbdialog_height") record = Settings.get_int_settings(self.session, "dbdialog_height")
if record.f_int != self.height(): if record.f_int != self.height():
record.update(self.session, {'f_int': self.height()}) record.update(self.session, {"f_int": self.height()})
record = Settings.get_int_settings(self.session, "dbdialog_width") record = Settings.get_int_settings(self.session, "dbdialog_width")
if record.f_int != self.width(): if record.f_int != self.width():
record.update(self.session, {'f_int': self.width()}) record.update(self.session, {"f_int": self.width()})
def add_selected(self) -> None: def add_selected(self) -> None:
"""Handle Add button""" """Handle Add button"""
if (not self.ui.matchList.selectedItems() and if not self.ui.matchList.selectedItems() and not self.ui.txtNote.text():
not self.ui.txtNote.text()):
return return
track = None track = None
@ -1946,10 +1931,12 @@ class DbDialog(QDialog):
if track: if track:
self.musicmuster.visible_playlist_tab().insert_track( self.musicmuster.visible_playlist_tab().insert_track(
self.session, track, note=self.ui.txtNote.text()) self.session, track, note=self.ui.txtNote.text()
)
else: else:
self.musicmuster.visible_playlist_tab().insert_header( self.musicmuster.visible_playlist_tab().insert_header(
self.session, note=self.ui.txtNote.text()) self.session, note=self.ui.txtNote.text()
)
# TODO: this shouldn't be needed as insert_track() saves # TODO: this shouldn't be needed as insert_track() saves
# playlist # playlist
@ -2041,11 +2028,11 @@ class SelectPlaylistDialog(QDialog):
self.session = session self.session = session
self.playlist = None self.playlist = None
record = Settings.get_int_settings( record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
self.session, "select_playlist_dialog_width")
width = record.f_int or 800 width = record.f_int or 800
record = Settings.get_int_settings( record = Settings.get_int_settings(
self.session, "select_playlist_dialog_height") self.session, "select_playlist_dialog_height"
)
height = record.f_int or 600 height = record.f_int or 600
self.resize(width, height) self.resize(width, height)
@ -2057,14 +2044,14 @@ class SelectPlaylistDialog(QDialog):
def __del__(self): # review def __del__(self): # review
record = Settings.get_int_settings( record = Settings.get_int_settings(
self.session, "select_playlist_dialog_height") self.session, "select_playlist_dialog_height"
)
if record.f_int != self.height(): if record.f_int != self.height():
record.update(self.session, {'f_int': self.height()}) record.update(self.session, {"f_int": self.height()})
record = Settings.get_int_settings( record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
self.session, "select_playlist_dialog_width")
if record.f_int != self.width(): if record.f_int != self.width():
record.update(self.session, {'f_int': self.width()}) record.update(self.session, {"f_int": self.width()})
def list_doubleclick(self, entry): # review def list_doubleclick(self, entry): # review
self.playlist = entry.data(Qt.ItemDataRole.UserRole) self.playlist = entry.data(Qt.ItemDataRole.UserRole)
@ -2086,12 +2073,22 @@ if __name__ == "__main__":
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
# Only allow at most one option to be specified # Only allow at most one option to be specified
group = p.add_mutually_exclusive_group() group = p.add_mutually_exclusive_group()
group.add_argument('-b', '--bitrates', group.add_argument(
action="store_true", dest="update_bitrates", "-b",
default=False, help="Update bitrates in database") "--bitrates",
group.add_argument('-c', '--check-database', action="store_true",
action="store_true", dest="check_db", dest="update_bitrates",
default=False, help="Check and report on database") default=False,
help="Update bitrates in database",
)
group.add_argument(
"-c",
"--check-database",
action="store_true",
dest="check_db",
default=False,
help="Check and report on database",
)
args = p.parse_args() args = p.parse_args()
# Run as required # Run as required
@ -2111,13 +2108,16 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
# PyQt6 defaults to a grey for labels # PyQt6 defaults to a grey for labels
palette = app.palette() palette = app.palette()
palette.setColor(QPalette.ColorRole.WindowText, palette.setColor(
QColor(Config.COLOUR_LABEL_TEXT)) QPalette.ColorRole.WindowText, QColor(Config.COLOUR_LABEL_TEXT)
)
# Set colours that will be used by playlist row stripes # Set colours that will be used by playlist row stripes
palette.setColor(QPalette.ColorRole.Base, palette.setColor(
QColor(Config.COLOUR_EVEN_PLAYLIST)) QPalette.ColorRole.Base, QColor(Config.COLOUR_EVEN_PLAYLIST)
palette.setColor(QPalette.ColorRole.AlternateBase, )
QColor(Config.COLOUR_ODD_PLAYLIST)) palette.setColor(
QPalette.ColorRole.AlternateBase, QColor(Config.COLOUR_ODD_PLAYLIST)
)
app.setPalette(palette) app.setPalette(palette)
win = Window() win = Window()
win.show() win.show()
@ -2129,8 +2129,12 @@ if __name__ == "__main__":
from helpers import send_mail from helpers import send_mail
msg = stackprinter.format(exc) msg = stackprinter.format(exc)
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, send_mail(
"Exception from musicmuster", msg) Config.ERRORS_TO,
Config.ERRORS_FROM,
"Exception from musicmuster",
msg,
)
print("\033[1;31;47mUnhandled exception starts") print("\033[1;31;47mUnhandled exception starts")
stackprinter.show(style="darkbg") stackprinter.show(style="darkbg")

File diff suppressed because it is too large Load Diff

View File

@ -10,11 +10,7 @@ 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,33 +74,31 @@ 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}"
@ -114,8 +108,9 @@ def main():
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(
@ -22,6 +22,7 @@ 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)
@ -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