Rebase dev onto v2_id
This commit is contained in:
parent
3a7b09f025
commit
a91309477b
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
10
app/log.py
10
app/log.py
@ -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()
|
||||||
|
|||||||
301
app/models.py
301
app/models.py
@ -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
|
||||||
|
|||||||
12
app/music.py
12
app/music.py
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
586
app/playlists.py
586
app/playlists.py
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user