Rebase dev onto v2_id

This commit is contained in:
Keith Edmunds 2022-03-02 09:25:59 +00:00
parent 3a7b09f025
commit a91309477b
10 changed files with 797 additions and 593 deletions

View File

@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="jdk" jdkName="Poetry (musicmuster) (2)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PyDocumentationSettings"> <component name="PyDocumentationSettings">

View File

@ -55,7 +55,9 @@ class Config(object):
NOTE_TIME_FORMAT = "%H:%M:%S" NOTE_TIME_FORMAT = "%H:%M:%S"
ROOT = os.environ.get('ROOT') or "/home/kae/music" ROOT = os.environ.get('ROOT') or "/home/kae/music"
TESTMODE = True TESTMODE = True
TOD_TIME_FORMAT = "%H:%M:%S"
TIMER_MS = 500 TIMER_MS = 500
TRACK_TIME_FORMAT = "%H:%M:%S"
VOLUME_VLC_DEFAULT = 75 VOLUME_VLC_DEFAULT = 75

View File

@ -4,22 +4,22 @@ import psutil
from config import Config from config import Config
from datetime import datetime from datetime import datetime
from pydub import AudioSegment from pydub import AudioSegment
from mutagen.flac import FLAC
from mutagen.mp3 import MP3
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from tinytag import TinyTag from tinytag import TinyTag
from typing import Dict, Optional, Union
def ask_yes_no(title, question): def ask_yes_no(title: str, question: str) -> bool:
"""Ask question; return True for yes, False for no""" """Ask question; return True for yes, False for no"""
button_reply = QMessageBox.question(None, title, question) button_reply: bool = QMessageBox.question(None, title, question)
return button_reply == QMessageBox.Yes return button_reply == QMessageBox.Yes
def fade_point(audio_segment, fade_threshold=0, def fade_point(
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): audio_segment: AudioSegment, fade_threshold: int = 0,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE):
""" """
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.
@ -30,15 +30,15 @@ def fade_point(audio_segment, fade_threshold=0,
assert chunk_size > 0 # to avoid infinite loop assert chunk_size > 0 # to avoid infinite loop
segment_length = audio_segment.duration_seconds * 1000 # ms segment_length: int = audio_segment.duration_seconds * 1000 # ms
trim_ms = segment_length - chunk_size trim_ms: int = segment_length - chunk_size
max_vol = audio_segment.dBFS max_vol: int = audio_segment.dBFS
if fade_threshold == 0: if fade_threshold == 0:
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
@ -46,7 +46,7 @@ def fade_point(audio_segment, fade_threshold=0,
return int(trim_ms) return int(trim_ms)
def get_audio_segment(path): 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)
@ -56,12 +56,12 @@ def get_audio_segment(path):
return None return None
def get_tags(path): def get_tags(path: str) -> Dict[str, Union[str, int]]:
""" """
Return a dictionary of title, artist, duration-in-milliseconds and path. Return a dictionary of title, artist, duration-in-milliseconds and path.
""" """
tag = TinyTag.get(path) tag: TinyTag = TinyTag.get(path)
return dict( return dict(
title=tag.title, title=tag.title,
@ -71,7 +71,8 @@ def get_tags(path):
) )
def get_relative_date(past_date, reference_date=None): def get_relative_date(past_date: datetime, reference_date: 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.
@ -91,6 +92,11 @@ def get_relative_date(past_date, reference_date=None):
if past_date > reference_date: if past_date > reference_date:
return "get_relative_date() past_date is after relative_date" return "get_relative_date() past_date is after relative_date"
days: int
days_str: str
weeks: int
weeks_str: str
weeks, days = divmod((reference_date.date() - past_date.date()).days, 7) weeks, days = divmod((reference_date.date() - past_date.date()).days, 7)
if weeks == days == 0: if weeks == days == 0:
# Played today, so return time instead # Played today, so return time instead
@ -106,8 +112,10 @@ def get_relative_date(past_date, reference_date=None):
return f"{weeks} {weeks_str}, {days} {days_str} ago" return f"{weeks} {weeks_str}, {days} {days_str} ago"
def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE, def leading_silence(
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): audio_segment: AudioSegment,
silence_threshold: int = Config.DBFS_SILENCE,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE):
""" """
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
@ -117,10 +125,10 @@ def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE,
https://github.com/jiaaro/pydub/blob/master/pydub/silence.py https://github.com/jiaaro/pydub/blob/master/pydub/silence.py
""" """
trim_ms = 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[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
silence_threshold and trim_ms < len(audio_segment)): silence_threshold and trim_ms < len(audio_segment)):
trim_ms += chunk_size trim_ms += chunk_size
@ -128,7 +136,13 @@ def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE,
return min(trim_ms, len(audio_segment)) return min(trim_ms, len(audio_segment))
def ms_to_mmss(ms, decimals=0, negative=False): def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
"""Convert milliseconds to mm:ss"""
minutes: int
remainder: int
seconds: float
if not ms: if not ms:
return "-" return "-"
sign = "" sign = ""
@ -149,7 +163,7 @@ def ms_to_mmss(ms, decimals=0, negative=False):
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}" return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
def open_in_audacity(path): def open_in_audacity(path: str) -> Optional[bool]:
""" """
Open passed file in Audacity Open passed file in Audacity
@ -164,19 +178,21 @@ def open_in_audacity(path):
if not path: if not path:
return False return False
to_pipe = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
from_pipe = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
EOL = '\n' eol: str = '\n'
def send_command(command): def send_command(command: str) -> None:
"""Send a single command.""" """Send a single command."""
to_audacity.write(command + EOL) to_audacity.write(command + eol)
to_audacity.flush() to_audacity.flush()
def get_response(): def get_response() -> str:
"""Return the command response.""" """Return the command response."""
result = ''
line = '' result: str = ''
line: str = ''
while True: while True:
result += line result += line
line = from_audacity.readline() line = from_audacity.readline()
@ -184,8 +200,9 @@ def open_in_audacity(path):
break break
return result return result
def do_command(command): def do_command(command: str) -> str:
"""Send one command, and return the response.""" """Send one command, and return the response."""
send_command(command) send_command(command)
response = get_response() response = get_response()
return response return response
@ -195,12 +212,15 @@ def open_in_audacity(path):
do_command(f'Import2: Filename="{path}"') do_command(f'Import2: Filename="{path}"')
def show_warning(title, msg): def show_warning(title: str, msg: str) -> None:
"Display a warning to user" """Display a warning to user"""
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel) QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def trailing_silence(audio_segment, silence_threshold=-50.0, def trailing_silence(
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): audio_segment: AudioSegment, silence_threshold: int = -50,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE):
"""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

@ -9,7 +9,7 @@ from config import Config
class LevelTagFilter(logging.Filter): class LevelTagFilter(logging.Filter):
"Add leveltag" """Add leveltag"""
def filter(self, record): def filter(self, record):
# Extract the first character of the level name # Extract the first character of the level name
@ -32,9 +32,9 @@ syslog = logging.handlers.SysLogHandler(address='/dev/log')
syslog.setLevel(Config.LOG_LEVEL_SYSLOG) syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
# Filter # Filter
filter = LevelTagFilter() local_filter = LevelTagFilter()
syslog.addFilter(filter) syslog.addFilter(local_filter)
stderr.addFilter(filter) stderr.addFilter(local_filter)
# create formatter and add it to the handlers # create formatter and add it to the handlers
stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s', stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s',
@ -103,6 +103,6 @@ if __name__ == "__main__":
return i() return i()
def i(): def i():
1 / 0 return 1 / 0
f() f()

View File

@ -6,6 +6,9 @@ import re
import sqlalchemy import sqlalchemy
from datetime import datetime from datetime import datetime
from typing import List, Optional
from pydub import AudioSegment
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy import ( from sqlalchemy import (
@ -23,7 +26,7 @@ from sqlalchemy.orm import (
backref, backref,
relationship, relationship,
sessionmaker, sessionmaker,
scoped_session scoped_session, RelationshipProperty
) )
from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
@ -48,7 +51,7 @@ engine = sqlalchemy.create_engine(
echo=Config.DISPLAY_SQL, echo=Config.DISPLAY_SQL,
pool_pre_ping=True) pool_pre_ping=True)
sm = sessionmaker(bind=engine) sm: sessionmaker = sessionmaker(bind=engine)
Session = scoped_session(sm) Session = scoped_session(sm)
Base: DeclarativeMeta = declarative_base() Base: DeclarativeMeta = declarative_base()
@ -63,16 +66,18 @@ def db_init():
class NoteColours(Base): class NoteColours(Base):
__tablename__ = 'notecolours' __tablename__ = 'notecolours'
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
substring = Column(String(256), index=False) substring: str = Column(String(256), index=False)
colour = Column(String(21), index=False) colour: str = Column(String(21), index=False)
enabled = Column(Boolean, default=True, index=True) enabled: bool = Column(Boolean, default=True, index=True)
is_regex = Column(Boolean, default=False, index=False) is_regex: bool = Column(Boolean, default=False, index=False)
is_casesensitive = Column(Boolean, default=False, index=False) is_casesensitive: bool = Column(Boolean, default=False, index=False)
order = Column(Integer, index=True) order: int = Column(Integer, index=True)
def __init__(self, session, substring, colour, enabled=True, def __init__(
is_regex=False, is_casesensitive=False, order=0): self, session: Session, substring: str, colour: str,
enabled: bool = True, is_regex: bool = False,
is_casesensitive: bool = False, order: int = 0) -> None:
self.substring = substring self.substring = substring
self.colour = colour self.colour = colour
self.enabled = enabled self.enabled = enabled
@ -83,36 +88,37 @@ class NoteColours(Base):
session.add(self) session.add(self)
session.commit() session.commit()
def __repr__(self): def __repr__(self) -> str:
return ( return (
f"<NoteColour(id={self.id}, substring={self.substring}, " f"<NoteColour(id={self.id}, substring={self.substring}, "
f"colour={self.colour}>" f"colour={self.colour}>"
) )
@classmethod @classmethod
def get_all(cls, session): def get_all(cls, session: Session) -> Optional[List["NoteColours"]]:
"""Return all records""" """Return all records"""
return session.query(cls).all() return session.query(cls).all()
@classmethod @classmethod
def get_by_id(cls, session, note_id): def get_by_id(cls, session: Session, note_id: int) -> \
Optional["NoteColours"]:
"""Return record identified by id, or None if not found""" """Return record identified by id, or None if not found"""
return session.query(NoteColours).filter( return session.query(NoteColours).local_filter(
NoteColours.id == note_id).first() NoteColours.id == note_id).first()
@staticmethod @staticmethod
def get_colour(session, text): def get_colour(session: Session, text: str) -> Optional[str]:
""" """
Parse text and return colour string if matched, else None Parse text and return colour string if matched, else None
""" """
for rec in ( for rec in (
session.query(NoteColours) session.query(NoteColours)
.filter(NoteColours.enabled.is_(True)) .local_filter(NoteColours.enabled.is_(True))
.order_by(NoteColours.order) .order_by(NoteColours.order)
.all() .all()
): ):
if rec.is_regex: if rec.is_regex:
flags = re.UNICODE flags = re.UNICODE
@ -135,14 +141,16 @@ class NoteColours(Base):
class Notes(Base): class Notes(Base):
__tablename__ = 'notes' __tablename__ = 'notes'
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
playlist_id = Column(Integer, ForeignKey('playlists.id')) playlist_id: int = Column(Integer, ForeignKey('playlists.id'))
playlist = relationship("Playlists", back_populates="notes", playlist: RelationshipProperty = relationship(
lazy="joined") "Playlists", back_populates="notes", lazy="joined")
row = Column(Integer, nullable=False) row: int = Column(Integer, nullable=False)
note = Column(String(256), index=False) note: str = Column(String(256), index=False)
def __init__(self, session, playlist_id, row, text): def __init__(
self, session: Session, playlist_id: int, row: int,
text: str) -> None:
"""Create note""" """Create note"""
DEBUG(f"Notes.__init__({playlist_id=}, {row=}, {text=})") DEBUG(f"Notes.__init__({playlist_id=}, {row=}, {text=})")
@ -152,12 +160,12 @@ class Notes(Base):
session.add(self) session.add(self)
session.commit() session.commit()
def __repr__(self): def __repr__(self) -> str:
return ( return (
f"<Note(id={self.id}, row={self.row}, note={self.note}>" f"<Note(id={self.id}, row={self.row}, note={self.note}>"
) )
def delete_note(self, session): def delete_note(self, session: Session) -> None:
"""Delete note""" """Delete note"""
DEBUG(f"delete_note({self.id=}") DEBUG(f"delete_note({self.id=}")
@ -165,30 +173,31 @@ class Notes(Base):
session.query(Notes).filter_by(id=self.id).delete() session.query(Notes).filter_by(id=self.id).delete()
session.commit() session.commit()
def update_note(self, session, row, text=None): def update_note(
self, session: Session, row: int,
text: Optional[str] = None) -> None:
""" """
Update note details. If text=None, don't change text. Update note details. If text=None, don't change text.
""" """
DEBUG(f"Notes.update_note({self.id=}, {row=}, {text=})") DEBUG(f"Notes.update_note({self.id=}, {row=}, {text=})")
note = session.query(Notes).filter_by(id=self.id).one() self.row = row
note.row = row
if text: if text:
note.note = text self.note = text
session.commit() session.commit()
class Playdates(Base): class Playdates(Base):
__tablename__ = 'playdates' __tablename__ = 'playdates'
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
lastplayed = Column(DateTime, index=True, default=None) lastplayed: datetime = Column(DateTime, index=True, default=None)
track_id = Column(Integer, ForeignKey('tracks.id')) track_id: int = Column(Integer, ForeignKey('tracks.id'))
tracks = relationship("Tracks", back_populates="playdates", tracks: RelationshipProperty = relationship(
lazy="joined") "Tracks", back_populates="playdates", lazy="joined")
def __init__(self, session, track): def __init__(self, session: Session, track: "Tracks") -> None:
"""Record that track was played""" """Record that track was played"""
DEBUG(f"add_playdate(track={track})") DEBUG(f"add_playdate(track={track})")
@ -200,11 +209,11 @@ class Playdates(Base):
session.commit() session.commit()
@staticmethod @staticmethod
def last_played(session, track_id): def last_played(session: Session, track_id: int) -> Optional[datetime]:
"""Return datetime track last played or None""" """Return datetime track last played or None"""
last_played = session.query(Playdates.lastplayed).filter( last_played: Optional[Playdates] = session.query(
(Playdates.track_id == track_id) Playdates.lastplayed).local_filter((Playdates.track_id == track_id)
).order_by(Playdates.lastplayed.desc()).first() ).order_by(Playdates.lastplayed.desc()).first()
if last_played: if last_played:
return last_played[0] return last_played[0]
@ -212,12 +221,12 @@ class Playdates(Base):
return None return None
@staticmethod @staticmethod
def remove_track(session, track_id): def remove_track(session: Session, track_id: int) -> None:
""" """
Remove all records of track_id Remove all records of track_id
""" """
session.query(Playdates).filter( session.query(Playdates).local_filter(
Playdates.track_id == track_id, Playdates.track_id == track_id,
).delete() ).delete()
session.commit() session.commit()
@ -230,32 +239,32 @@ class Playlists(Base):
__tablename__ = "playlists" __tablename__ = "playlists"
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(32), nullable=False, unique=True) name: str = Column(String(32), nullable=False, unique=True)
last_used = Column(DateTime, default=None, nullable=True) last_used: datetime = Column(DateTime, default=None, nullable=True)
loaded = Column(Boolean, default=True, nullable=False) loaded: bool = Column(Boolean, default=True, nullable=False)
notes = relationship("Notes", notes = relationship(
order_by="Notes.row", "Notes", order_by="Notes.row", back_populates="playlist", lazy="joined")
back_populates="playlist",
lazy="joined")
tracks = association_proxy('playlist_tracks', 'tracks') tracks = association_proxy('playlist_tracks', 'tracks')
row = association_proxy('playlist_tracks', 'row') row = association_proxy('playlist_tracks', 'row')
def __init__(self, session, name): def __init__(self, session: Session, name: str) -> None:
self.name = name self.name = name
session.add(self) session.add(self)
session.commit() session.commit()
def __repr__(self): def __repr__(self) -> str:
return f"<Playlists(id={self.id}, name={self.name}>" return f"<Playlists(id={self.id}, name={self.name}>"
def add_note(self, session, row, text): def add_note(self, session: Session, row: int, text: str) -> Notes:
"""Add note to playlist at passed row""" """Add note to playlist at passed row"""
return Notes(session, self.id, row, text) return Notes(session, self.id, row, text)
def add_track(self, session, track, row=None): def add_track(
self, session: Session, track: "Tracks",
row: Optional[int] = None) -> None:
""" """
Add track to playlist at given row. Add track to playlist at given row.
If row=None, add to end of playlist If row=None, add to end of playlist
@ -266,7 +275,7 @@ class Playlists(Base):
PlaylistTracks(session, self.id, track.id, row) PlaylistTracks(session, self.id, track.id, row)
def close(self, session): def close(self, session: Session) -> None:
"""Record playlist as no longer loaded""" """Record playlist as no longer loaded"""
self.loaded = False self.loaded = False
@ -274,41 +283,41 @@ class Playlists(Base):
session.commit() session.commit()
@classmethod @classmethod
def get_all(cls, session): def get_all(cls, session: Session) -> List["Playlists"]:
"""Returns a list of all playlists ordered by last use""" """Returns a list of all playlists ordered by last use"""
return ( return (
session.query(cls) session.query(cls)
.order_by(cls.last_used.desc()) .order_by(cls.last_used.desc())
).all() ).all()
@classmethod @classmethod
def get_by_id(cls, session, playlist_id): def get_by_id(cls, session: Session, playlist_id: int) -> "Playlists":
return (session.query(cls).filter(cls.id == playlist_id)).one() return (session.query(cls).local_filter(cls.id == playlist_id)).one()
@classmethod @classmethod
def get_closed(cls, session): def get_closed(cls, session: Session) -> List["Playlists"]:
"""Returns a list of all closed playlists ordered by last use""" """Returns a list of all closed playlists ordered by last use"""
return ( return (
session.query(cls) session.query(cls)
.filter(cls.loaded.is_(False)) .local_filter(cls.loaded.is_(False))
.order_by(cls.last_used.desc()) .order_by(cls.last_used.desc())
).all() ).all()
@classmethod @classmethod
def get_open(cls, session): def get_open(cls, session: Session) -> List["Playlists"]:
""" """
Return a list of playlists marked "loaded", ordered by loaded date. Return a list of playlists marked "loaded", ordered by loaded date.
""" """
return ( return (
session.query(cls) session.query(cls)
.filter(cls.loaded.is_(True)) .local_filter(cls.loaded.is_(True))
.order_by(cls.last_used.desc()) .order_by(cls.last_used.desc())
).all() ).all()
def mark_open(self, session): def mark_open(self, session: Session) -> None:
"""Mark playlist as loaded and used now""" """Mark playlist as loaded and used now"""
self.loaded = True self.loaded = True
@ -316,21 +325,20 @@ class Playlists(Base):
session.add(self) session.add(self)
session.commit() session.commit()
def remove_all_tracks(self, session): def remove_all_tracks(self, session: Session) -> None:
""" """
Remove all tracks from this playlist Remove all tracks from this playlist
""" """
session.query(PlaylistTracks).filter( session.query(PlaylistTracks).local_filter(
PlaylistTracks.playlist_id == self.id, PlaylistTracks.playlist_id == self.id,
).delete() ).delete()
session.commit() session.commit()
def remove_track(self, session, row): def remove_track(self, session: Session, row: int) -> None:
DEBUG(f"Playlist.remove_track({self.id=}, {row=})") DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
session.query(PlaylistTracks).filter( session.query(PlaylistTracks).local_filter(
PlaylistTracks.playlist_id == self.id, PlaylistTracks.playlist_id == self.id,
PlaylistTracks.row == row PlaylistTracks.row == row
).delete() ).delete()
@ -340,12 +348,13 @@ class Playlists(Base):
class PlaylistTracks(Base): class PlaylistTracks(Base):
__tablename__ = 'playlist_tracks' __tablename__ = 'playlist_tracks'
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
playlist_id = Column(Integer, ForeignKey('playlists.id'), primary_key=True) playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
track_id = Column(Integer, ForeignKey('tracks.id'), primary_key=True) primary_key=True)
row = Column(Integer, nullable=False) track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True)
tracks = relationship("Tracks") row: int = Column(Integer, nullable=False)
playlist = relationship( tracks: RelationshipProperty = relationship("Tracks")
playlist: RelationshipProperty = relationship(
Playlists, Playlists,
backref=backref( backref=backref(
"playlist_tracks", "playlist_tracks",
@ -354,26 +363,33 @@ class PlaylistTracks(Base):
) )
) )
def __init__(self, session, playlist_id, track_id, row): def __init__(
self, session: Session, playlist_id: int, track_id: int,
row: int) -> None:
DEBUG(f"PlaylistTracks.__init__({playlist_id=}, {track_id=}, {row=})") DEBUG(f"PlaylistTracks.__init__({playlist_id=}, {track_id=}, {row=})")
self.playlist_id = playlist_id, self.playlist_id = playlist_id
self.track_id = track_id, self.track_id = track_id
self.row = row self.row = row
session.add(self) session.add(self)
session.commit() session.commit()
@staticmethod @staticmethod
def move_track(session, from_playlist_id, row, to_playlist_id): def move_track(
session: Session, from_playlist_id: int, row: int,
to_playlist_id: int) -> None:
""" """
Move track between playlists. This would be more efficient with Move track between playlists. This would be more efficient with
an ORM-enabled UPDATE statement, but this works just fine. an ORM-enabled UPDATE statement, but this works just fine.
""" """
DEBUG( DEBUG(
"PlaylistTracks.move_tracks(" "PlaylistTracks.move_tracks("
f"{from_playlist_id=}, {rows=}, {to_playlist_id=})" f"{from_playlist_id=}, {row=}, {to_playlist_id=})"
) )
max_row = session.query(func.max(PlaylistTracks.row)).filter(
new_row: int
max_row: Optional[int] = session.query(
func.max(PlaylistTracks.row)).local_filter(
PlaylistTracks.playlist_id == to_playlist_id).scalar() PlaylistTracks.playlist_id == to_playlist_id).scalar()
if max_row is None: if max_row is None:
# Destination playlist is empty; use row 0 # Destination playlist is empty; use row 0
@ -382,7 +398,7 @@ class PlaylistTracks(Base):
# Destination playlist has tracks; add to end # Destination playlist has tracks; add to end
new_row = max_row + 1 new_row = max_row + 1
try: try:
record = session.query(PlaylistTracks).filter( record: PlaylistTracks = session.query(PlaylistTracks).local_filter(
PlaylistTracks.playlist_id == from_playlist_id, PlaylistTracks.playlist_id == from_playlist_id,
PlaylistTracks.row == row).one() PlaylistTracks.row == row).one()
except NoResultFound: except NoResultFound:
@ -397,10 +413,12 @@ class PlaylistTracks(Base):
session.commit() session.commit()
@staticmethod @staticmethod
def next_free_row(session, playlist): def next_free_row(session: Session, playlist: Playlists) -> int:
"""Return next free row number""" """Return next free row number"""
last_row = session.query( row: int
last_row: int = session.query(
func.max(PlaylistTracks.row) func.max(PlaylistTracks.row)
).filter_by(playlist_id=playlist.id).first() ).filter_by(playlist_id=playlist.id).first()
# if there are no rows, the above returns (None, ) which is True # if there are no rows, the above returns (None, ) which is True
@ -415,16 +433,20 @@ class PlaylistTracks(Base):
class Settings(Base): class Settings(Base):
__tablename__ = 'settings' __tablename__ = 'settings'
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(32), nullable=False, unique=True) name: str = Column(String(32), nullable=False, unique=True)
f_datetime = Column(DateTime, default=None, nullable=True) f_datetime: datetime = Column(DateTime, default=None, nullable=True)
f_int = Column(Integer, default=None, nullable=True) f_int: int = Column(Integer, default=None, nullable=True)
f_string = Column(String(128), default=None, nullable=True) f_string: str = Column(String(128), default=None, nullable=True)
@classmethod @classmethod
def get_int(cls, session, name): def get_int_settings(cls, session: Session, name: str) -> "Settings":
"""Get setting for an integer or return new setting record"""
int_setting: Settings
try: try:
int_setting = session.query(cls).filter( int_setting = session.query(cls).local_filter(
cls.name == name).one() cls.name == name).one()
except NoResultFound: except NoResultFound:
int_setting = Settings() int_setting = Settings()
@ -434,7 +456,7 @@ class Settings(Base):
session.commit() session.commit()
return int_setting return int_setting
def update(self, session, data): def update(self, session: Session, data):
for key, value in data.items(): for key, value in data.items():
assert hasattr(self, key) assert hasattr(self, key)
setattr(self, key, value) setattr(self, key, value)
@ -444,46 +466,48 @@ class Settings(Base):
class Tracks(Base): class Tracks(Base):
__tablename__ = 'tracks' __tablename__ = 'tracks'
id = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(256), index=True) title: str = Column(String(256), index=True)
artist = Column(String(256), index=True) artist: str = Column(String(256), index=True)
duration = Column(Integer, index=True) duration: int = Column(Integer, index=True)
start_gap = Column(Integer, index=False) start_gap: int = Column(Integer, index=False)
fade_at = Column(Integer, index=False) fade_at: int = Column(Integer, index=False)
silence_at = Column(Integer, index=False) silence_at: int = Column(Integer, index=False)
path = Column(String(2048), index=False, nullable=False) path: str = Column(String(2048), index=False, nullable=False)
mtime = Column(Float, index=True) mtime: float = Column(Float, index=True)
lastplayed = Column(DateTime, index=True, default=None) lastplayed: datetime = Column(DateTime, index=True, default=None)
playlists = relationship("PlaylistTracks", back_populates="tracks", playlists: RelationshipProperty = relationship("PlaylistTracks",
lazy="joined") back_populates="tracks",
playdates = relationship("Playdates", back_populates="tracks", lazy="joined")
lazy="joined") playdates: RelationshipProperty = relationship("Playdates",
back_populates="tracks",
lazy="joined")
def __init__(self, session, path): def __init__(self, session: Session, path: str) -> None:
self.path = path self.path = path
session.add(self) session.add(self)
session.commit() session.commit()
def __repr__(self): def __repr__(self) -> str:
return ( return (
f"<Track(id={self.id}, title={self.title}, " f"<Track(id={self.id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>" f"artist={self.artist}, path={self.path}>"
) )
@staticmethod @staticmethod
def get_all_paths(session): def get_all_paths(session) -> List[str]:
"""Return a list of paths of all tracks""" """Return a list of paths of all tracks"""
return [a[0] for a in session.query(Tracks.path).all()] return [a[0] for a in session.query(Tracks.path).all()]
@classmethod @classmethod
def get_all_tracks(cls, session): def get_all_tracks(cls, session: Session) -> List["Tracks"]:
"""Return a list of all tracks""" """Return a list of all tracks"""
return session.query(cls).all() return session.query(cls).all()
@classmethod @classmethod
def get_or_create(cls, session, path): def get_or_create(cls, session: Session, path: str) -> "Tracks":
""" """
If a track with path exists, return it; If a track with path exists, return it;
else created new track and return it else created new track and return it
@ -492,14 +516,15 @@ class Tracks(Base):
DEBUG(f"Tracks.get_or_create({path=})") DEBUG(f"Tracks.get_or_create({path=})")
try: try:
track = session.query(cls).filter(cls.path == path).one() track = session.query(cls).local_filter(cls.path == path).one()
except NoResultFound: except NoResultFound:
track = Tracks(session, path) track = Tracks(session, path)
return track return track
@classmethod @classmethod
def get_from_filename(cls, session, filename): def get_from_filename(cls, session: Session, filename: str) \
-> Optional["Tracks"]:
""" """
Return track if one and only one track in database has passed Return track if one and only one track in database has passed
filename (ie, basename of path). Return None if zero or more filename (ie, basename of path). Return None if zero or more
@ -508,93 +533,93 @@ class Tracks(Base):
DEBUG(f"Tracks.get_track_from_filename({filename=})") DEBUG(f"Tracks.get_track_from_filename({filename=})")
try: try:
track = session.query(Tracks).filter(Tracks.path.ilike( track = session.query(Tracks).local_filter(Tracks.path.ilike(
f'%{os.path.sep}{filename}')).one() f'%{os.path.sep}{filename}')).one()
return track return track
except (NoResultFound, MultipleResultsFound): except (NoResultFound, MultipleResultsFound):
return None return None
@classmethod @classmethod
def get_from_path(cls, session, path): def get_from_path(cls, session: Session, path: str) -> List["Tracks"]:
""" """
Return track with passee path, or None. Return track with passee path, or None.
""" """
DEBUG(f"Tracks.get_track_from_path({path=})") DEBUG(f"Tracks.get_track_from_path({path=})")
return session.query(Tracks).filter(Tracks.path == path).first() return session.query(Tracks).local_filter(Tracks.path == path).first()
@classmethod @classmethod
def get_by_id(cls, session, track_id): def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]:
"""Return track or None""" """Return track or None"""
try: try:
DEBUG(f"Tracks.get_track(track_id={track_id})") DEBUG(f"Tracks.get_track(track_id={track_id})")
track = session.query(Tracks).filter(Tracks.id == track_id).one() track = session.query(Tracks).local_filter(Tracks.id == track_id).one()
return track return track
except NoResultFound: except NoResultFound:
ERROR(f"get_track({track_id}): not found") ERROR(f"get_track({track_id}): not found")
return None return None
def rescan(self, session): def rescan(self, session: Session) -> None:
""" """
Update audio metadata for passed track. Update audio metadata for passed track.
""" """
audio = get_audio_segment(self.path) audio: AudioSegment = get_audio_segment(self.path)
self.duration = len(audio) self.duration = len(audio)
self.fade_at = round(fade_point(audio) / 1000, self.fade_at = round(fade_point(audio) / 1000,
Config.MILLISECOND_SIGFIGS) * 1000 Config.MILLISECOND_SIGFIGS) * 1000
self.mtime = os.path.getmtime(self.path) self.mtime = os.path.getmtime(self.path)
self.silence_at = round(trailing_silence(audio) / 1000, self.silence_at = round(trailing_silence(audio) / 1000,
Config.MILLISECOND_SIGFIGS) * 1000 Config.MILLISECOND_SIGFIGS) * 1000
self.start_gap = leading_silence(audio) self.start_gap = leading_silence(audio)
session.add(self) session.add(self)
session.commit() session.commit()
@staticmethod @staticmethod
def remove_by_path(session, path): def remove_by_path(session: Session, path: str) -> None:
"""Remove track with passed path from database""" """Remove track with passed path from database"""
DEBUG(f"Tracks.remove_path({path=})") DEBUG(f"Tracks.remove_path({path=})")
try: try:
session.query(Tracks).filter(Tracks.path == path).delete() session.query(Tracks).local_filter(Tracks.path == path).delete()
session.commit() session.commit()
except IntegrityError as exception: except IntegrityError as exception:
ERROR(f"Can't remove track with {path=} ({exception=})") ERROR(f"Can't remove track with {path=} ({exception=})")
@classmethod @classmethod
def search_artists(cls, session, text): def search_artists(cls, session: Session, text: str) -> List["Tracks"]:
return ( return (
session.query(cls) session.query(cls)
.filter(cls.artist.ilike(f"%{text}%")) .local_filter(cls.artist.ilike(f"%{text}%"))
.order_by(cls.title) .order_by(cls.title)
).all() ).all()
@classmethod @classmethod
def search_titles(cls, session, text): def search_titles(cls, session: Session, text: str) -> List["Tracks"]:
return ( return (
session.query(cls) session.query(cls)
.filter(cls.title.ilike(f"%{text}%")) .local_filter(cls.title.ilike(f"%{text}%"))
.order_by(cls.title) .order_by(cls.title)
).all() ).all()
def update_lastplayed(self, session): def update_lastplayed(self, session: Session) -> None:
self.lastplayed = datetime.now() self.lastplayed = datetime.now()
session.add(self) session.add(self)
session.commit() session.commit()
def update_artist(self, session, artist): def update_artist(self, session: Session, artist: str) -> None:
self.artist = artist self.artist = artist
session.add(self) session.add(self)
session.commit() session.commit()
def update_title(self, session, title): def update_title(self, session: Session, title: str) -> None:
self.title = title self.title = title
session.add(self) session.add(self)
session.commit() session.commit()
def update_path(self, newpath): def update_path(self, newpath: str) -> None:
self.path = newpath self.path = newpath

View File

@ -86,13 +86,11 @@ class Music:
p.stop() p.stop()
DEBUG(f"Releasing player {p=}", True) DEBUG(f"Releasing player {p=}", True)
p.release() p.release()
# Ensure we don't reference player after release
p = None
self.fading -= 1 self.fading -= 1
def get_playtime(self): def get_playtime(self):
"Return elapsed play time" """Return elapsed play time"""
with lock: with lock:
if not self.player: if not self.player:
@ -101,7 +99,7 @@ class Music:
return self.player.get_time() return self.player.get_time()
def get_position(self): def get_position(self):
"Return current position" """Return current position"""
with lock: with lock:
DEBUG("music.get_position", True) DEBUG("music.get_position", True)
@ -147,13 +145,13 @@ class Music:
return self.fading > 0 return self.fading > 0
def set_position(self, ms): def set_position(self, ms):
"Set current play time in milliseconds from start" """Set current play time in milliseconds from start"""
with lock: with lock:
return self.player.set_time(ms) return self.player.set_time(ms)
def set_volume(self, volume): def set_volume(self, volume):
"Set maximum volume used for player" """Set maximum volume used for player"""
with lock: with lock:
if not self.player: if not self.player:
@ -163,7 +161,7 @@ class Music:
self.player.audio_set_volume(volume) self.player.audio_set_volume(volume)
def stop(self): def stop(self):
"Immediately stop playing" """Immediately stop playing"""
DEBUG(f"music.stop(), {self.player=}", True) DEBUG(f"music.stop(), {self.player=}", True)

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import webbrowser import webbrowser
import os
import psutil import psutil
import sys import sys
import urllib.parse import urllib.parse
@ -9,8 +8,9 @@ import urllib.parse
from datetime import datetime, timedelta from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION from log import DEBUG, EXCEPTION
from typing import Callable, Dict, List, Optional, Tuple
from PyQt5.QtCore import QProcess, Qt, QTimer, QUrl from PyQt5.QtCore import QEvent, QProcess, Qt, QTimer, QUrl
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@ -37,38 +37,39 @@ from ui.main_window_ui import Ui_MainWindow
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None): def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
self.timer = QTimer() self.timer: QTimer = QTimer()
self.even_tick = True self.even_tick: bool = True
self.playing = False self.playing: bool = False
self.connect_signals_slots() self.connect_signals_slots()
self.disable_play_next_controls() self.disable_play_next_controls()
self.music = music.Music() self.music: music.Music = music.Music()
self.current_track = None self.current_track: Optional[Tracks] = None
self.current_track_playlist_tab = None self.current_track_playlist_tab: Optional[PlaylistTab] = None
self.info_tabs = {} self.info_tabs: Optional[Dict[str, QWebView]] = {}
self.next_track = None self.next_track: Optional[Tracks] = None
self.next_track_playlist_tab = None self.next_track_playlist_tab: Optional[PlaylistTab] = None
self.previous_track = None self.previous_track: Optional[Tracks] = None
self.previous_track_position = None self.previous_track_position: Optional[int] = None
self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT) self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT)
self.set_main_window_size() self.set_main_window_size()
self.lblSumPlaytime = QLabel("") self.lblSumPlaytime: QLabel = QLabel("")
self.statusbar.addPermanentWidget(self.lblSumPlaytime) self.statusbar.addPermanentWidget(self.lblSumPlaytime)
self.visible_playlist_tab = self.tabPlaylist.currentWidget self.visible_playlist_tab: Callable[[], PlaylistTab] = \
self.tabPlaylist.currentWidget
self.load_last_playlists() self.load_last_playlists()
self.enable_play_next_controls() self.enable_play_next_controls()
self.check_audacity() self.check_audacity()
self.timer.start(Config.TIMER_MS) self.timer.start(Config.TIMER_MS)
def add_file(self): def add_file(self) -> None:
# TODO: V2 enahancement to import tracks # TODO: V2 enahancement to import tracks
dlg = QFileDialog() dlg = QFileDialog()
dlg.setFileMode(QFileDialog.ExistingFiles) dlg.setFileMode(QFileDialog.ExistingFiles)
@ -85,22 +86,23 @@ class Window(QMainWindow, Ui_MainWindow):
# also be saved to database # also be saved to database
self.visible_playlist_tab().insert_track(session, track) self.visible_playlist_tab().insert_track(session, track)
def set_main_window_size(self): def set_main_window_size(self) -> None:
# TODO: V2 check
"""Set size of window from database""" """Set size of window from database"""
with Session() as session: with Session() as session:
record = Settings.get_int(session, "mainwindow_x") record = Settings.get_int_settings(session, "mainwindow_x")
x = record.f_int or 1 x = record.f_int or 1
record = Settings.get_int(session, "mainwindow_y") record = Settings.get_int_settings(session, "mainwindow_y")
y = record.f_int or 1 y = record.f_int or 1
record = Settings.get_int(session, "mainwindow_width") record = Settings.get_int_settings(session, "mainwindow_width")
width = record.f_int or 1599 width = record.f_int or 1599
record = Settings.get_int(session, "mainwindow_height") record = Settings.get_int_settings(session, "mainwindow_height")
height = record.f_int or 981 height = record.f_int or 981
self.setGeometry(x, y, width, height) self.setGeometry(x, y, width, height)
@staticmethod @staticmethod
def check_audacity(): def check_audacity() -> None:
"""Offer to run Audacity if not running""" """Offer to run Audacity if not running"""
if not Config.CHECK_AUDACITY_AT_STARTUP: if not Config.CHECK_AUDACITY_AT_STARTUP:
@ -113,10 +115,12 @@ class Window(QMainWindow, Ui_MainWindow):
QProcess.startDetached(Config.AUDACITY_COMMAND, []) QProcess.startDetached(Config.AUDACITY_COMMAND, [])
def clear_selection(self): def clear_selection(self):
""" Clear selected row"""
if self.visible_playlist_tab(): if self.visible_playlist_tab():
self.visible_playlist_tab().clearSelection() self.visible_playlist_tab().clearSelection()
def closeEvent(self, event): def closeEvent(self, event: QEvent) -> None:
"""Don't allow window to close when a track is playing""" """Don't allow window to close when a track is playing"""
if self.music.playing(): if self.music.playing():
@ -129,32 +133,32 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("closeEvent() accepted") DEBUG("closeEvent() accepted")
with Session() as session: with Session() as session:
record = Settings.get_int(session, "mainwindow_height") record = Settings.get_int_settings(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(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(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(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()})
# Find a playlist tab (as opposed to an info tab) and # Find a playlist tab (as opposed to an info tab) and
# save column widths # save column widths
if self.current_track_playlist_tab: if self.current_track_playlist_tab:
self.current_track_playlist_tab.close(session) self.current_track_playlist_tab.close()
elif self.next_track_playlist_tab: elif self.next_track_playlist_tab:
self.next_track_playlist_tab.close(session) self.next_track_playlist_tab.close()
event.accept() event.accept()
def connect_signals_slots(self): def connect_signals_slots(self) -> None:
self.actionAdd_file.triggered.connect(self.add_file) self.actionAdd_file.triggered.connect(self.add_file)
self.actionAdd_note.triggered.connect(self.create_note) self.actionAdd_note.triggered.connect(self.create_note)
self.action_Clear_selection.triggered.connect(self.clear_selection) self.action_Clear_selection.triggered.connect(self.clear_selection)
@ -189,7 +193,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def create_playlist(self): def create_playlist(self) -> None:
"""Create new playlist""" """Create new playlist"""
dlg = QInputDialog(self) dlg = QInputDialog(self)
@ -202,17 +206,23 @@ class Window(QMainWindow, Ui_MainWindow):
playlist = Playlists(session, dlg.textValue()) playlist = Playlists(session, dlg.textValue())
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def change_volume(self, volume): def change_volume(self, volume: int) -> None:
"""Change player maximum volume""" """Change player maximum volume"""
DEBUG(f"change_volume({volume})") DEBUG(f"change_volume({volume})")
self.music.set_volume(volume) self.music.set_volume(volume)
def close_playlist_tab(self): def close_playlist_tab(self) -> None:
"""Close active playlist tab"""
self.close_tab(self.tabPlaylist.currentIndex()) self.close_tab(self.tabPlaylist.currentIndex())
def close_tab(self, index): def close_tab(self, index: int) -> None:
"""
Close tab unless it holds the curren or next track
"""
if hasattr(self.tabPlaylist.widget(index), 'playlist'): if hasattr(self.tabPlaylist.widget(index), 'playlist'):
if self.tabPlaylist.widget(index) == ( if self.tabPlaylist.widget(index) == (
self.current_track_playlist_tab): self.current_track_playlist_tab):
@ -230,7 +240,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Close regardless of tab type # Close regardless of tab type
self.tabPlaylist.removeTab(index) self.tabPlaylist.removeTab(index)
def create_note(self): def create_note(self) -> None:
"""Call playlist to create note""" """Call playlist to create note"""
try: try:
@ -239,49 +249,73 @@ class Window(QMainWindow, Ui_MainWindow):
# Just return if there's no visible playlist tab # Just return if there's no visible playlist tab
return return
def disable_play_next_controls(self): def disable_play_next_controls(self) -> None:
"""
Disable "play next" keyboard controls
"""
DEBUG("disable_play_next_controls()") DEBUG("disable_play_next_controls()")
self.actionPlay_next.setEnabled(False) self.actionPlay_next.setEnabled(False)
self.statusbar.showMessage("Play controls: Disabled", 0) self.statusbar.showMessage("Play controls: Disabled", 0)
def enable_play_next_controls(self): def enable_play_next_controls(self) -> None:
"""
Enable "play next" keyboard controls
"""
DEBUG("enable_play_next_controls()") DEBUG("enable_play_next_controls()")
self.actionPlay_next.setEnabled(True) self.actionPlay_next.setEnabled(True)
self.statusbar.showMessage("Play controls: Enabled", 0) self.statusbar.showMessage("Play controls: Enabled", 0)
def end_of_track_actions(self): def end_of_track_actions(self) -> None:
"""Clean up after track played""" """
Clean up after track played
# Set self.playing to False so that tick() doesn't see Actions required:
# player=None and kick off end-of-track actions - Set flag to say we're not playing a track
- Reset current track
- Tell playlist_tab track has finished
- Reset current playlist_tab
- Reset clocks
- Update headers
- Enable controls
"""
# Set flag to say we're not playing a track so that tick()
# doesn't see player=None and kick off end-of-track actions
self.playing = False self.playing = False
# Clean up metadata # Reset current track
if self.current_track: if self.current_track:
self.previous_track = self.current_track self.previous_track = self.current_track
self.current_track = None self.current_track = None
# Tell playlist_tab track has finished and
# reset current playlist_tab
if self.current_track_playlist_tab: if self.current_track_playlist_tab:
self.current_track_playlist_tab.play_stopped() self.current_track_playlist_tab.play_stopped()
self.current_track_playlist_tab = None self.current_track_playlist_tab = None
# Clean up display # Reset clocks
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
self.label_silent_timer.setText("00:00") self.label_silent_timer.setText("00:00")
self.frame_silent.setStyleSheet("") self.frame_silent.setStyleSheet("")
self.label_end_timer.setText("00:00") self.label_end_timer.setText("00:00")
# Update headers
self.update_headers() self.update_headers()
# Enable controls # Enable controls
self.enable_play_next_controls() self.enable_play_next_controls()
def export_playlist_tab(self): def export_playlist_tab(self) -> None:
"""Export the current playlist to an m3u file""" """Export the current playlist to an m3u file"""
if not self.visible_playlist_tab(): if not self.visible_playlist_tab():
return return
# Get output filename # Get output filename
pathspec = QFileDialog.getSaveFileName( pathspec: Tuple[str, str] = QFileDialog.getSaveFileName(
self, 'Save Playlist', self, 'Save Playlist',
directory=f"{self.visible_playlist_tab().name}.m3u", directory=f"{self.visible_playlist_tab().name}.m3u",
filter="M3U files (*.m3u);;All files (*.*)" filter="M3U files (*.m3u);;All files (*.*)"
@ -289,7 +323,7 @@ class Window(QMainWindow, Ui_MainWindow):
if not pathspec: if not pathspec:
return return
path = pathspec[0] path: str = pathspec[0]
if not path.endswith(".m3u"): if not path.endswith(".m3u"):
path += ".m3u" path += ".m3u"
@ -307,7 +341,7 @@ class Window(QMainWindow, Ui_MainWindow):
"\n" "\n"
) )
def fade(self): def fade(self) -> None:
"""Fade currently playing track""" """Fade currently playing track"""
DEBUG("musicmuster:fade()", True) DEBUG("musicmuster:fade()", True)
@ -340,18 +374,19 @@ class Window(QMainWindow, Ui_MainWindow):
for playlist in Playlists.get_open(session): for playlist in Playlists.get_open(session):
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session, playlist): def create_playlist_tab(self, session: Session,
playlist: Playlists) -> None:
""" """
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. add tab to display.
""" """
playlist_tab = PlaylistTab(parent=self, playlist_tab: PlaylistTab = PlaylistTab(
session=session, playlist=playlist) parent=self, session=session, playlist=playlist)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
def move_selected(self): def move_selected(self) -> None:
"""Move selected rows to another playlist""" """Move selected rows to another playlist"""
with Session() as session: with Session() as session:
@ -396,12 +431,14 @@ class Window(QMainWindow, Ui_MainWindow):
# Update source playlist # Update source playlist
self.visible_playlist_tab().remove_rows(rows) self.visible_playlist_tab().remove_rows(rows)
def open_info_tab(self, title_list): def open_info_tabs(self) -> None:
""" """
Ensure we have info tabs for each of the passed titles Ensure we have info tabs for next and current track titles
Called from update_headers
""" """
title_list: List[str, str] = [self.previous_track.title,
self.current_track.title]
for title in title_list: for title in title_list:
if title in self.info_tabs.keys(): if title in self.info_tabs.keys():
# We already have a tab for this track # We already have a tab for this track
@ -433,19 +470,24 @@ class Window(QMainWindow, Ui_MainWindow):
url = Config.INFO_TAB_URL % txt url = Config.INFO_TAB_URL % txt
widget.setUrl(QUrl(url)) widget.setUrl(QUrl(url))
def play_next(self): def play_next(self) -> None:
""" """
Play next track. Play next track.
If there is no next track set, return. Actions required:
If there's currently a track playing, fade it. - If there is no next track set, return.
Move next track to current track. - If there's currently a track playing, fade it.
Play (new) current. - Move next track to current track.
Tell playlist to update "current track" metadata. This will also - Update record of current track playlist_tab
trigger a call to - If current track on different playlist_tab to last, reset
Cue up next track in playlist if there is one. last track playlist_tab colour
Tell database to record it as played - Set current track playlist_tab colour
Update metadata and headers, and repaint - Play (new) current track.
- Tell database to record it as played
- Tell playlist track is now playing
- Disable play next controls
- Update headers
- Update clocks
""" """
DEBUG( DEBUG(
@ -462,43 +504,63 @@ class Window(QMainWindow, Ui_MainWindow):
return return
with Session() as session: with Session() as session:
# Stop current track, if any # If there's currently a track playing, fade it.
self.stop_playing() self.stop_playing(fade=True)
# Play next track # Move next track to current track.
self.current_track = self.next_track self.current_track = self.next_track
self.next_track = None self.next_track = None
# If current track on different playlist_tab to last, reset
# last track playlist_tab colour
# Set current track playlist_tab colour
if self.current_track_playlist_tab != self.next_track_playlist_tab:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Update record of current track playlist_tab
self.current_track_playlist_tab = self.next_track_playlist_tab self.current_track_playlist_tab = self.next_track_playlist_tab
self.next_track_playlist_tab = None self.next_track_playlist_tab = None
# Set current track playlist_tab colour
self.set_tab_colour(self.current_track_playlist_tab, self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_CURRENT_TAB)) QColor(Config.COLOUR_CURRENT_TAB))
self.music.play(self.current_track.path)
# Tell playlist so it can update its display # Play (new) current track
self.current_track_playlist_tab.play_started() self.music.play(self.current_track.path)
# Tell database to record it as played # Tell database to record it as played
Playdates(session, self.current_track) Playdates(session, self.current_track)
# Tell playlist track is now playing
self.current_track_playlist_tab.play_started()
# Disable play next controls
self.disable_play_next_controls() self.disable_play_next_controls()
# Update headers
self.update_headers() self.update_headers()
# Set time clocks # Update clocks
now = datetime.now() now = datetime.now()
self.label_start_tod.setText(now.strftime("%H:%M:%S")) self.label_start_tod.setText(now.strftime("%H:%M:%S"))
silence_at = self.current_track.silence_at silence_at = self.current_track.silence_at
silence_time = now + timedelta(milliseconds=silence_at) silence_time = now + timedelta(milliseconds=silence_at)
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
self.label_fade_length.setText(helpers.ms_to_mmss( self.label_fade_length.setText(
silence_at - self.current_track.fade_at helpers.ms_to_mmss(silence_at - self.current_track.fade_at)
)) )
def search_database(self) -> None:
"""Show dialog box to select and cue track from database"""
def search_database(self):
with Session() as session: with Session() as session:
dlg = DbDialog(self, session) dlg = DbDialog(self, session)
dlg.exec() dlg.exec()
def open_playlist(self): def open_playlist(self) -> None:
"""Select and activate existing playlist"""
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)
@ -507,35 +569,35 @@ class Window(QMainWindow, Ui_MainWindow):
playlist = Playlists.get_by_id(session, dlg.plid) playlist = Playlists.get_by_id(session, dlg.plid)
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def select_next_row(self): def select_next_row(self) -> None:
"""Select next or first row in playlist""" """Select next or first row in playlist"""
self.visible_playlist_tab().select_next_row() self.visible_playlist_tab().select_next_row()
def select_played(self): def select_played(self) -> None:
"""Select all played tracks in playlist""" """Select all played tracks in playlist"""
self.visible_playlist_tab().select_played_tracks() self.visible_playlist_tab().select_played_tracks()
def select_previous_row(self): def select_previous_row(self) -> None:
"""Select previous or first row in playlist""" """Select previous or first row in playlist"""
self.visible_playlist_tab().select_previous_row() self.visible_playlist_tab().select_previous_row()
def select_unplayed(self): def select_unplayed(self) -> None:
"""Select all unplayed tracks in playlist""" """Select all unplayed tracks in playlist"""
self.visible_playlist_tab().select_unplayed_tracks() self.visible_playlist_tab().select_unplayed_tracks()
def set_tab_colour(self, widget, colour): def set_tab_colour(self, widget, colour) -> None:
""" """
Find the tab containing the widget and set the text colour Find the tab containing the widget and set the text colour
""" """
idx = self.tabPlaylist.indexOf(widget) idx: int = self.tabPlaylist.indexOf(widget)
self.tabPlaylist.tabBar().setTabTextColor(idx, colour) self.tabPlaylist.tabBar().setTabTextColor(idx, colour)
def song_info_search(self): def song_info_search(self) -> None:
""" """
Open browser tab for Wikipedia, searching for Open browser tab for Wikipedia, searching for
the first that exists of: the first that exists of:
@ -544,7 +606,7 @@ class Window(QMainWindow, Ui_MainWindow):
- current track - current track
""" """
title = self.visible_playlist_tab().get_selected_title() title: Optional[str] = self.visible_playlist_tab().get_selected_title()
if not title: if not title:
if self.next_track: if self.next_track:
title = self.next_track.title title = self.next_track.title
@ -552,36 +614,36 @@ class Window(QMainWindow, Ui_MainWindow):
if self.current_track: if self.current_track:
title = self.current_track.title title = self.current_track.title
if title: if title:
# Wikipedia
txt = urllib.parse.quote_plus(title) txt = urllib.parse.quote_plus(title)
url = Config.INFO_TAB_URL % txt url = Config.INFO_TAB_URL % txt
webbrowser.open(url, new=2) webbrowser.open(url, new=2)
def stop(self): def stop(self) -> None:
"""Stop playing immediately""" """Stop playing immediately"""
DEBUG("musicmuster.stop()") DEBUG("musicmuster.stop()")
self.stop_playing(fade=False) self.stop_playing(fade=False)
def stop_playing(self, fade=True): def stop_playing(self, fade=True) -> None:
"""Stop playing current track""" """
Stop playing current track
Actions required:
- Return if not playing
- Stop/fade track
- Reset playlist_tab colour
- Run end-of-track actions
"""
DEBUG(f"musicmuster.stop_playing({fade=})", True) DEBUG(f"musicmuster.stop_playing({fade=})", True)
# Set tab colour # Return if not playing
if self.current_track_playlist_tab == self.next_track_playlist_tab:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
else:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
if not self.music.playing(): if not self.music.playing():
DEBUG("musicmuster.stop_playing(): not playing", True) DEBUG("musicmuster.stop_playing(): not playing", True)
self.end_of_track_actions()
return return
# Stop/fade track
self.previous_track_position = self.music.get_position() self.previous_track_position = self.music.get_position()
if fade: if fade:
DEBUG("musicmuster.stop_playing(): fading music", True) DEBUG("musicmuster.stop_playing(): fading music", True)
@ -590,35 +652,51 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop_playing(): stopping music", True) DEBUG("musicmuster.stop_playing(): stopping music", True)
self.music.stop() self.music.stop()
# Reset playlist_tab colour
if self.current_track_playlist_tab == self.next_track_playlist_tab:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
else:
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Run end-of-track actions
self.end_of_track_actions() self.end_of_track_actions()
# Release player def this_is_the_next_track(self, playlist_tab: PlaylistTab,
self.music.stop() track: Tracks) -> None:
self.update_headers()
def this_is_the_next_track(self, playlist_tab, track):
""" """
This is notification from a playlist tab that it holds the next This is notification from a playlist tab that it holds the next
track to be played. track to be played.
Actions required:
- Clear next track if on other tab
- Reset tab colour if on other tab
- Note next playlist tab
- Set next playlist_tab tab colour
- Note next track
- Update headers
- Populate info tabs
""" """
# The next track has been selected on the playlist_tab # Clear next track if on another tab
# playlist. However, there may already be a 'next track'
# selected on another playlist that the user is overriding,
# in which case we need to reset that playlist.
if self.next_track_playlist_tab != playlist_tab: if self.next_track_playlist_tab != playlist_tab:
# We need to reset the ex-next-track playlist # We need to reset the ex-next-track playlist
if self.next_track_playlist_tab: if self.next_track_playlist_tab:
self.next_track_playlist_tab.clear_next() self.next_track_playlist_tab.clear_next()
# Reset tab colour if it NOT the current playing tab
# Reset tab colour if on other tab
if (self.next_track_playlist_tab != if (self.next_track_playlist_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_NORMAL_TAB)) QColor(Config.COLOUR_NORMAL_TAB))
# Note next playlist tab
self.next_track_playlist_tab = playlist_tab self.next_track_playlist_tab = playlist_tab
# self.next_track_playlist_tab is now set to correct playlist
# Set the colour of the next playlist tab if it isn't the # Set next playlist_tab tab colour if it isn't the
# currently-playing tab # currently-playing tab
if (self.next_track_playlist_tab != if (self.next_track_playlist_tab !=
self.current_track_playlist_tab): self.current_track_playlist_tab):
@ -626,13 +704,18 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track_playlist_tab, self.next_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB)) QColor(Config.COLOUR_NEXT_TAB))
# Note next track
self.next_track = track self.next_track = track
# Update headers
self.update_headers() self.update_headers()
def tick(self): # Populate 'info' tabs
self.open_info_tabs()
def tick(self) -> None:
""" """
Update screen Carry out clock tick actions.
The Time of Day clock is updated every tick (500ms). The Time of Day clock is updated every tick (500ms).
@ -640,22 +723,27 @@ class Window(QMainWindow, Ui_MainWindow):
one-second resolution, updating every 500ms can result in some one-second resolution, updating every 500ms can result in some
timers updating and then, 500ms later, other timers updating. That timers updating and then, 500ms later, other timers updating. That
looks odd. looks odd.
Actions required:
- Update TOD clock
- If track is playing, update track clocks time and colours
- Else: run stop_track
""" """
now = datetime.now() # Update TOD clock
self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
self.lblTOD.setText(now.strftime("%H:%M:%S"))
self.even_tick = not self.even_tick self.even_tick = not self.even_tick
if not self.even_tick: if not self.even_tick:
return return
# If track is playing, update track clocks time and colours
if self.music.player and self.music.playing(): if self.music.player and self.music.playing():
self.playing = True self.playing = True
playtime = self.music.get_playtime() playtime: int = self.music.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime) time_to_fade: int = (self.current_track.fade_at - playtime)
time_to_silence = (self.current_track.silence_at - playtime) time_to_silence: int = (self.current_track.silence_at - playtime)
time_to_end = (self.current_track.duration - playtime) time_to_end: int = (self.current_track.duration - playtime)
# Elapsed time # Elapsed time
if time_to_end < 500: if time_to_end < 500:
@ -702,19 +790,13 @@ class Window(QMainWindow, Ui_MainWindow):
if self.playing: if self.playing:
self.stop_playing() self.stop_playing()
def update_headers(self): def update_headers(self) -> None:
""" """Update last / current / next track headers"""
Update last / current / next track headers.
Ensure a Wikipedia tab for each title.
"""
titles = []
try: try:
self.hdrPreviousTrack.setText( self.hdrPreviousTrack.setText(
f"{self.previous_track.title} - {self.previous_track.artist}" f"{self.previous_track.title} - {self.previous_track.artist}"
) )
titles.append(self.previous_track.title)
except AttributeError: except AttributeError:
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
@ -722,7 +804,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.hdrCurrentTrack.setText( self.hdrCurrentTrack.setText(
f"{self.current_track.title} - {self.current_track.artist}" f"{self.current_track.title} - {self.current_track.artist}"
) )
titles.append(self.current_track.title)
except AttributeError: except AttributeError:
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
@ -730,17 +811,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.hdrNextTrack.setText( self.hdrNextTrack.setText(
f"{self.next_track.title} - {self.next_track.artist}" f"{self.next_track.title} - {self.next_track.artist}"
) )
titles.append(self.next_track.title)
except AttributeError: except AttributeError:
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")
self.open_info_tab(titles)
class DbDialog(QDialog): class DbDialog(QDialog):
"""Select track from database""" """Select track from database"""
def __init__(self, parent, session): def __init__(self, parent, session): # review
super().__init__(parent) super().__init__(parent)
self.session = session self.session = session
self.ui = Ui_Dialog() self.ui = Ui_Dialog()
@ -753,22 +831,22 @@ class DbDialog(QDialog):
self.ui.radioTitle.toggled.connect(self.title_artist_toggle) self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
self.ui.searchString.textEdited.connect(self.chars_typed) self.ui.searchString.textEdited.connect(self.chars_typed)
record = Settings.get_int(self.session, "dbdialog_width") record = Settings.get_int_settings(self.session, "dbdialog_width")
width = record.f_int or 800 width = record.f_int or 800
record = Settings.get_int(self.session, "dbdialog_height") record = Settings.get_int_settings(self.session, "dbdialog_height")
height = record.f_int or 600 height = record.f_int or 600
self.resize(width, height) self.resize(width, height)
def __del__(self): def __del__(self): # review
record = Settings.get_int(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(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): def add_selected(self): # review
if not self.ui.matchList.selectedItems(): if not self.ui.matchList.selectedItems():
return return
@ -776,11 +854,11 @@ class DbDialog(QDialog):
track = item.data(Qt.UserRole) track = item.data(Qt.UserRole)
self.add_track(track) self.add_track(track)
def add_selected_and_close(self): def add_selected_and_close(self): # review
self.add_selected() self.add_selected()
self.close() self.close()
def title_artist_toggle(self): def title_artist_toggle(self): # review
""" """
Handle switching between searching for artists and searching for Handle switching between searching for artists and searching for
titles titles
@ -789,7 +867,7 @@ class DbDialog(QDialog):
# Logic is handled already in chars_typed(), so just call that. # Logic is handled already in chars_typed(), so just call that.
self.chars_typed(self.ui.searchString.text()) self.chars_typed(self.ui.searchString.text())
def chars_typed(self, s): def chars_typed(self, s): # review
if len(s) > 0: if len(s) > 0:
if self.ui.radioTitle.isChecked(): if self.ui.radioTitle.isChecked():
matches = Tracks.search_titles(self.session, s) matches = Tracks.search_titles(self.session, s)
@ -806,24 +884,24 @@ class DbDialog(QDialog):
t.setData(Qt.UserRole, track) t.setData(Qt.UserRole, track)
self.ui.matchList.addItem(t) self.ui.matchList.addItem(t)
def double_click(self, entry): def double_click(self, entry): # review
track = entry.data(Qt.UserRole) track = entry.data(Qt.UserRole)
self.add_track(track) self.add_track(track)
# Select search text to make it easier for next search # Select search text to make it easier for next search
self.select_searchtext() self.select_searchtext()
def add_track(self, track): def add_track(self, track): # review
# Add to playlist on screen # Add to playlist on screen
self.parent().visible_playlist_tab().insert_track( self.parent().visible_playlist_tab().insert_track(
self.session, track) self.session, track)
# Select search text to make it easier for next search # Select search text to make it easier for next search
self.select_searchtext() self.select_searchtext()
def select_searchtext(self): def select_searchtext(self): # review
self.ui.searchString.selectAll() self.ui.searchString.selectAll()
self.ui.searchString.setFocus() self.ui.searchString.setFocus()
def selection_changed(self): def selection_changed(self): # review
if not self.ui.matchList.selectedItems(): if not self.ui.matchList.selectedItems():
return return
@ -833,7 +911,7 @@ class DbDialog(QDialog):
class SelectPlaylistDialog(QDialog): class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlists=None): def __init__(self, parent=None, playlists=None): # review
super().__init__(parent) super().__init__(parent)
if playlists is None: if playlists is None:
@ -846,9 +924,9 @@ class SelectPlaylistDialog(QDialog):
self.plid = None self.plid = None
with Session() as session: with Session() as session:
record = Settings.get_int(session, "select_playlist_dialog_width") record = Settings.get_int_settings(session, "select_playlist_dialog_width")
width = record.f_int or 800 width = record.f_int or 800
record = Settings.get_int(session, "select_playlist_dialog_height") record = Settings.get_int_settings(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)
@ -858,21 +936,21 @@ class SelectPlaylistDialog(QDialog):
p.setData(Qt.UserRole, plid) p.setData(Qt.UserRole, plid)
self.ui.lstPlaylists.addItem(p) self.ui.lstPlaylists.addItem(p)
def __del__(self): def __del__(self): # review
with Session() as session: with Session() as session:
record = Settings.get_int(session, "select_playlist_dialog_height") record = Settings.get_int_settings(session, "select_playlist_dialog_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(session, "select_playlist_dialog_width") record = Settings.get_int_settings(session, "select_playlist_dialog_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()})
def list_doubleclick(self, entry): def list_doubleclick(self, entry): # review
self.plid = entry.data(Qt.UserRole) self.plid = entry.data(Qt.UserRole)
self.accept() self.accept()
def open(self): def open(self): # review
if self.ui.lstPlaylists.selectedItems(): if self.ui.lstPlaylists.selectedItems():
item = self.ui.lstPlaylists.currentItem() item = self.ui.lstPlaylists.currentItem()
self.plid = item.data(Qt.UserRole) self.plid = item.data(Qt.UserRole)

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@ PyQtWebEngine = "^5.15.5"
pydub = "^0.25.1" pydub = "^0.25.1"
PyQt5-sip = "^12.9.1" PyQt5-sip = "^12.9.1"
mypy = "^0.931" mypy = "^0.931"
sqlalchemy-stubs = "^0.4"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = "^0.931" mypy = "^0.931"

View File

@ -340,7 +340,7 @@ def test_playlisttracks_move_track(session):
assert len(tracks1) == 1 assert len(tracks1) == 1
assert len(tracks2) == 1 assert len(tracks2) == 1
assert tracks1[track1_row] == track1 assert tracks1[track1_row] == track1
assert tracks2[track2_row] == track2 assert tracks2[0] == track2
def test_tracks_get_all_paths(session): def test_tracks_get_all_paths(session):