Flake8 and Black run on all files
This commit is contained in:
parent
f44d6aa25e
commit
986257bef6
13
.flake8
Normal file
13
.flake8
Normal file
@ -0,0 +1,13 @@
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
select = C,E,F,W,B,B950
|
||||
extend-ignore = E203, E501
|
||||
exclude =
|
||||
.git
|
||||
app/ui,
|
||||
__pycache__,
|
||||
archive,
|
||||
migrations,
|
||||
prof,
|
||||
docs,
|
||||
app/icons_rc.py
|
||||
@ -2,14 +2,12 @@
|
||||
|
||||
from PyQt6.QtCore import Qt, QEvent, QObject
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemDelegate,
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QStyledItemDelegate,
|
||||
QStyleOptionViewItem,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
)
|
||||
@ -33,16 +31,15 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
if event.type() == QEvent.Type.KeyPress:
|
||||
key_event = cast(QKeyEvent, event)
|
||||
if key_event.key() == Qt.Key.Key_Return:
|
||||
if key_event.modifiers() == (
|
||||
Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier):
|
||||
print("save data")
|
||||
self.commitData.emit(editor)
|
||||
self.closeEditor.emit(editor)
|
||||
return True
|
||||
elif key_event.key() == Qt.Key.Key_Escape:
|
||||
discard_edits = QMessageBox.question(
|
||||
self.parent(), "Abandon edit", "Discard changes?")
|
||||
self.parent(), "Abandon edit", "Discard changes?"
|
||||
)
|
||||
if discard_edits == QMessageBox.StandardButton.Yes:
|
||||
print("abandon edit")
|
||||
self.closeEditor.emit(editor)
|
||||
@ -74,7 +71,7 @@ class MainWindow(QMainWindow):
|
||||
self.table_widget.resizeRowsToContents()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from pydub import AudioSegment, effects
|
||||
from pydub import AudioSegment
|
||||
|
||||
# DIR = "/home/kae/git/musicmuster/archive"
|
||||
DIR = "/home/kae/git/musicmuster"
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from config import Config
|
||||
from contextlib import contextmanager
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import (sessionmaker, scoped_session)
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from typing import Generator
|
||||
|
||||
from log import log
|
||||
|
||||
MYSQL_CONNECT = os.environ.get('MM_DB')
|
||||
MYSQL_CONNECT = os.environ.get("MM_DB")
|
||||
if MYSQL_CONNECT is None:
|
||||
raise ValueError("MYSQL_CONNECT is undefined")
|
||||
else:
|
||||
dbname = MYSQL_CONNECT.split('/')[-1]
|
||||
dbname = MYSQL_CONNECT.split("/")[-1]
|
||||
log.debug(f"Database: {dbname}")
|
||||
|
||||
# MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION')
|
||||
@ -31,10 +30,10 @@ else:
|
||||
|
||||
engine = create_engine(
|
||||
MYSQL_CONNECT,
|
||||
encoding='utf-8',
|
||||
encoding="utf-8",
|
||||
echo=Config.DISPLAY_SQL,
|
||||
pool_pre_ping=True,
|
||||
future=True
|
||||
future=True,
|
||||
)
|
||||
|
||||
|
||||
@ -47,8 +46,7 @@ def Session() -> Generator[scoped_session, None, None]:
|
||||
Session = scoped_session(sessionmaker(bind=engine, future=True))
|
||||
log.debug(f"SqlA: session acquired [{hex(id(Session))}]")
|
||||
log.debug(
|
||||
f"Session acquisition: {file}:{function}:{lineno} "
|
||||
f"[{hex(id(Session))}]"
|
||||
f"Session acquisition: {file}:{function}:{lineno} " f"[{hex(id(Session))}]"
|
||||
)
|
||||
yield Session
|
||||
log.debug(f" SqlA: session released [{hex(id(Session))}]")
|
||||
|
||||
111
app/helpers.py
111
app/helpers.py
@ -1,4 +1,3 @@
|
||||
import numpy as np
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
@ -16,7 +15,7 @@ from pydub import AudioSegment, effects
|
||||
from pydub.utils import mediainfo
|
||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
||||
from tinytag import TinyTag # type: ignore
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
||||
@ -26,7 +25,8 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
||||
dlg.setWindowTitle(title)
|
||||
dlg.setText(question)
|
||||
dlg.setStandardButtons(
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
dlg.setIcon(QMessageBox.Icon.Question)
|
||||
if default_yes:
|
||||
dlg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
||||
@ -36,8 +36,10 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
||||
|
||||
|
||||
def fade_point(
|
||||
audio_segment: AudioSegment, fade_threshold: float = 0.0,
|
||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
|
||||
audio_segment: AudioSegment,
|
||||
fade_threshold: float = 0.0,
|
||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
|
||||
) -> int:
|
||||
"""
|
||||
Returns the millisecond/index of the point where the volume drops below
|
||||
the maximum and doesn't get louder again.
|
||||
@ -55,8 +57,9 @@ def fade_point(
|
||||
fade_threshold = max_vol
|
||||
|
||||
while (
|
||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
||||
and trim_ms > 0): # noqa W503
|
||||
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
|
||||
and trim_ms > 0
|
||||
): # noqa W503
|
||||
trim_ms -= chunk_size
|
||||
|
||||
# if there is no trailing silence, return lenght of track (it's less
|
||||
@ -77,9 +80,9 @@ def file_is_unreadable(path: Optional[str]) -> bool:
|
||||
|
||||
def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
||||
try:
|
||||
if path.endswith('.mp3'):
|
||||
if path.endswith(".mp3"):
|
||||
return AudioSegment.from_mp3(path)
|
||||
elif path.endswith('.flac'):
|
||||
elif path.endswith(".flac"):
|
||||
return AudioSegment.from_file(path, "flac") # type: ignore
|
||||
except AttributeError:
|
||||
return None
|
||||
@ -99,12 +102,13 @@ def get_tags(path: str) -> Dict[str, Any]:
|
||||
artist=tag.artist,
|
||||
bitrate=round(tag.bitrate),
|
||||
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
||||
path=path
|
||||
path=path,
|
||||
)
|
||||
|
||||
|
||||
def get_relative_date(past_date: Optional[datetime],
|
||||
reference_date: Optional[datetime] = None) -> str:
|
||||
def get_relative_date(
|
||||
past_date: Optional[datetime], reference_date: Optional[datetime] = None
|
||||
) -> str:
|
||||
"""
|
||||
Return how long before reference_date past_date is as string.
|
||||
|
||||
@ -147,7 +151,8 @@ def get_relative_date(past_date: Optional[datetime],
|
||||
def leading_silence(
|
||||
audio_segment: AudioSegment,
|
||||
silence_threshold: int = Config.DBFS_SILENCE,
|
||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
|
||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
|
||||
) -> int:
|
||||
"""
|
||||
Returns the millisecond/index that the leading silence ends.
|
||||
audio_segment - the segment to find silence in
|
||||
@ -159,9 +164,11 @@ def leading_silence(
|
||||
|
||||
trim_ms: int = 0 # ms
|
||||
assert chunk_size > 0 # to avoid infinite loop
|
||||
while (
|
||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
|
||||
silence_threshold and trim_ms < len(audio_segment)):
|
||||
while audio_segment[
|
||||
trim_ms : trim_ms + chunk_size
|
||||
].dBFS < silence_threshold and trim_ms < len( # noqa W504
|
||||
audio_segment
|
||||
):
|
||||
trim_ms += chunk_size
|
||||
|
||||
# if there is no end it should return the length of the segment
|
||||
@ -175,9 +182,9 @@ def send_mail(to_addr, from_addr, subj, body):
|
||||
msg = EmailMessage()
|
||||
msg.set_content(body)
|
||||
|
||||
msg['Subject'] = subj
|
||||
msg['From'] = from_addr
|
||||
msg['To'] = to_addr
|
||||
msg["Subject"] = subj
|
||||
msg["From"] = from_addr
|
||||
msg["To"] = to_addr
|
||||
|
||||
# Send the message via SMTP server.
|
||||
context = ssl.create_default_context()
|
||||
@ -194,8 +201,7 @@ def send_mail(to_addr, from_addr, subj, body):
|
||||
s.quit()
|
||||
|
||||
|
||||
def ms_to_mmss(ms: Optional[int], decimals: int = 0,
|
||||
negative: bool = False) -> str:
|
||||
def ms_to_mmss(ms: Optional[int], decimals: int = 0, negative: bool = False) -> str:
|
||||
"""Convert milliseconds to mm:ss"""
|
||||
|
||||
minutes: int
|
||||
@ -227,13 +233,12 @@ def normalise_track(path):
|
||||
|
||||
# Check type
|
||||
ftype = os.path.splitext(path)[1][1:]
|
||||
if ftype not in ['mp3', 'flac']:
|
||||
if ftype not in ["mp3", "flac"]:
|
||||
log.info(
|
||||
f"helpers.normalise_track({path}): "
|
||||
f"File type {ftype} not implemented"
|
||||
f"helpers.normalise_track({path}): " f"File type {ftype} not implemented"
|
||||
)
|
||||
|
||||
bitrate = mediainfo(path)['bit_rate']
|
||||
bitrate = mediainfo(path)["bit_rate"]
|
||||
audio = get_audio_segment(path)
|
||||
if not audio:
|
||||
return
|
||||
@ -245,23 +250,20 @@ def normalise_track(path):
|
||||
_, temp_path = tempfile.mkstemp()
|
||||
shutil.copyfile(path, temp_path)
|
||||
except Exception as err:
|
||||
log.debug(
|
||||
f"helpers.normalise_track({path}): err1: {repr(err)}"
|
||||
)
|
||||
log.debug(f"helpers.normalise_track({path}): err1: {repr(err)}")
|
||||
return
|
||||
|
||||
# Overwrite original file with normalised output
|
||||
normalised = effects.normalize(audio)
|
||||
try:
|
||||
normalised.export(path, format=os.path.splitext(path)[1][1:],
|
||||
bitrate=bitrate)
|
||||
normalised.export(path, format=os.path.splitext(path)[1][1:], bitrate=bitrate)
|
||||
# Fix up permssions and ownership
|
||||
os.chown(path, stats.st_uid, stats.st_gid)
|
||||
os.chmod(path, stats.st_mode)
|
||||
# Copy tags
|
||||
if ftype == 'flac':
|
||||
if ftype == "flac":
|
||||
tag_handler = FLAC
|
||||
elif ftype == 'mp3':
|
||||
elif ftype == "mp3":
|
||||
tag_handler = MP3
|
||||
else:
|
||||
return
|
||||
@ -271,9 +273,7 @@ def normalise_track(path):
|
||||
dst[tag] = src[tag]
|
||||
dst.save()
|
||||
except Exception as err:
|
||||
log.debug(
|
||||
f"helpers.normalise_track({path}): err2: {repr(err)}"
|
||||
)
|
||||
log.debug(f"helpers.normalise_track({path}): err2: {repr(err)}")
|
||||
# Restore original file
|
||||
shutil.copyfile(path, temp_path)
|
||||
finally:
|
||||
@ -296,9 +296,9 @@ def open_in_audacity(path: str) -> bool:
|
||||
if not path:
|
||||
return False
|
||||
|
||||
to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
|
||||
from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
|
||||
eol: str = '\n'
|
||||
to_pipe: str = "/tmp/audacity_script_pipe.to." + str(os.getuid())
|
||||
from_pipe: str = "/tmp/audacity_script_pipe.from." + str(os.getuid())
|
||||
eol: str = "\n"
|
||||
|
||||
def send_command(command: str) -> None:
|
||||
"""Send a single command."""
|
||||
@ -308,13 +308,13 @@ def open_in_audacity(path: str) -> bool:
|
||||
def get_response() -> str:
|
||||
"""Return the command response."""
|
||||
|
||||
result: str = ''
|
||||
line: str = ''
|
||||
result: str = ""
|
||||
line: str = ""
|
||||
|
||||
while True:
|
||||
result += line
|
||||
line = from_audacity.readline()
|
||||
if line == '\n' and len(result) > 0:
|
||||
if line == "\n" and len(result) > 0:
|
||||
break
|
||||
return result
|
||||
|
||||
@ -325,8 +325,7 @@ def open_in_audacity(path: str) -> bool:
|
||||
response = get_response()
|
||||
return response
|
||||
|
||||
with open(to_pipe, 'w') as to_audacity, open(
|
||||
from_pipe, 'rt') as from_audacity:
|
||||
with open(to_pipe, "w") as to_audacity, open(from_pipe, "rt") as from_audacity:
|
||||
do_command(f'Import2: Filename="{path}"')
|
||||
|
||||
return True
|
||||
@ -338,18 +337,18 @@ def set_track_metadata(session, track):
|
||||
t = get_tags(track.path)
|
||||
audio = get_audio_segment(track.path)
|
||||
|
||||
track.title = t['title']
|
||||
track.artist = t['artist']
|
||||
track.bitrate = t['bitrate']
|
||||
track.title = t["title"]
|
||||
track.artist = t["artist"]
|
||||
track.bitrate = t["bitrate"]
|
||||
|
||||
if not audio:
|
||||
return
|
||||
track.duration = len(audio)
|
||||
track.start_gap = leading_silence(audio)
|
||||
track.fade_at = round(fade_point(audio) / 1000,
|
||||
Config.MILLISECOND_SIGFIGS) * 1000
|
||||
track.silence_at = round(trailing_silence(audio) / 1000,
|
||||
Config.MILLISECOND_SIGFIGS) * 1000
|
||||
track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
|
||||
track.silence_at = (
|
||||
round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
|
||||
)
|
||||
track.mtime = os.path.getmtime(track.path)
|
||||
|
||||
session.commit()
|
||||
@ -358,20 +357,20 @@ def set_track_metadata(session, track):
|
||||
def show_OK(parent: QMainWindow, title: str, msg: str) -> None:
|
||||
"""Display a message to user"""
|
||||
|
||||
QMessageBox.information(parent, title, msg,
|
||||
buttons=QMessageBox.StandardButton.Ok)
|
||||
QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok)
|
||||
|
||||
|
||||
def show_warning(parent: QMainWindow, title: str, msg: str) -> None:
|
||||
"""Display a warning to user"""
|
||||
|
||||
QMessageBox.warning(parent, title, msg,
|
||||
buttons=QMessageBox.StandardButton.Cancel)
|
||||
QMessageBox.warning(parent, title, msg, buttons=QMessageBox.StandardButton.Cancel)
|
||||
|
||||
|
||||
def trailing_silence(
|
||||
audio_segment: AudioSegment, silence_threshold: int = -50,
|
||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
|
||||
audio_segment: AudioSegment,
|
||||
silence_threshold: int = -50,
|
||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
|
||||
) -> int:
|
||||
"""Return fade point from start in milliseconds"""
|
||||
|
||||
return fade_point(audio_segment, silence_threshold, chunk_size)
|
||||
|
||||
@ -2,7 +2,7 @@ import urllib.parse
|
||||
|
||||
from datetime import datetime
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
from PyQt6.QtCore import QUrl # type: ignore
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
@ -47,13 +47,11 @@ class InfoTabs(QTabWidget):
|
||||
|
||||
if url in self.tabtitles.values():
|
||||
self.setCurrentIndex(
|
||||
list(self.tabtitles.keys())[
|
||||
list(self.tabtitles.values()).index(url)
|
||||
]
|
||||
list(self.tabtitles.keys())[list(self.tabtitles.values()).index(url)]
|
||||
)
|
||||
return
|
||||
|
||||
short_title = title[:Config.INFO_TAB_TITLE_LENGTH]
|
||||
short_title = title[: Config.INFO_TAB_TITLE_LENGTH]
|
||||
|
||||
if self.count() < Config.MAX_INFO_TABS:
|
||||
# Create a new tab
|
||||
@ -63,9 +61,7 @@ class InfoTabs(QTabWidget):
|
||||
|
||||
else:
|
||||
# Reuse oldest widget
|
||||
widget = min(
|
||||
self.last_update, key=self.last_update.get # type: ignore
|
||||
)
|
||||
widget = min(self.last_update, key=self.last_update.get) # type: ignore
|
||||
tab_index = self.indexOf(widget)
|
||||
self.setTabText(tab_index, short_title)
|
||||
|
||||
|
||||
260
app/models.py
260
app/models.py
@ -1,13 +1,11 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import stackprinter # type: ignore
|
||||
|
||||
from dbconfig import Session, scoped_session
|
||||
from dbconfig import scoped_session
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Iterable, List, Optional, Union, ValuesView
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
@ -35,21 +33,14 @@ from sqlalchemy.orm.exc import (
|
||||
from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
)
|
||||
from config import Config
|
||||
from helpers import (
|
||||
fade_point,
|
||||
get_audio_segment,
|
||||
get_tags,
|
||||
leading_silence,
|
||||
trailing_silence,
|
||||
)
|
||||
from log import log
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# Database classes
|
||||
class Carts(Base):
|
||||
__tablename__ = 'carts'
|
||||
__tablename__ = "carts"
|
||||
|
||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||
cart_number: int = Column(Integer, nullable=False, unique=True)
|
||||
@ -64,10 +55,15 @@ class Carts(Base):
|
||||
f"name={self.name}, path={self.path}>"
|
||||
)
|
||||
|
||||
def __init__(self, session: scoped_session, cart_number: int,
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
cart_number: int,
|
||||
name: Optional[str] = None,
|
||||
duration: Optional[int] = None, path: Optional[str] = None,
|
||||
enabled: bool = True) -> None:
|
||||
duration: Optional[int] = None,
|
||||
path: Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
) -> None:
|
||||
"""Create new cart"""
|
||||
|
||||
self.cart_number = cart_number
|
||||
@ -81,7 +77,7 @@ class Carts(Base):
|
||||
|
||||
|
||||
class NoteColours(Base):
|
||||
__tablename__ = 'notecolours'
|
||||
__tablename__ = "notecolours"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
substring = Column(String(256), index=False)
|
||||
@ -106,11 +102,15 @@ class NoteColours(Base):
|
||||
if not text:
|
||||
return None
|
||||
|
||||
for rec in session.execute(
|
||||
for rec in (
|
||||
session.execute(
|
||||
select(NoteColours)
|
||||
.filter(NoteColours.enabled.is_(True))
|
||||
.order_by(NoteColours.order)
|
||||
).scalars().all():
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
):
|
||||
if rec.is_regex:
|
||||
flags = re.UNICODE
|
||||
if not rec.is_casesensitive:
|
||||
@ -130,11 +130,11 @@ class NoteColours(Base):
|
||||
|
||||
|
||||
class Playdates(Base):
|
||||
__tablename__ = 'playdates'
|
||||
__tablename__ = "playdates"
|
||||
|
||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||
lastplayed = Column(DateTime, index=True, default=None)
|
||||
track_id = Column(Integer, ForeignKey('tracks.id'))
|
||||
track_id = Column(Integer, ForeignKey("tracks.id"))
|
||||
track: "Tracks" = relationship("Tracks", back_populates="playdates")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@ -152,8 +152,7 @@ class Playdates(Base):
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def last_played(session: scoped_session,
|
||||
track_id: int) -> Optional[datetime]:
|
||||
def last_played(session: scoped_session, track_id: int) -> Optional[datetime]:
|
||||
"""Return datetime track last played or None"""
|
||||
|
||||
last_played = session.execute(
|
||||
@ -169,8 +168,7 @@ class Playdates(Base):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def played_after(session: scoped_session,
|
||||
since: datetime) -> List["Playdates"]:
|
||||
def played_after(session: scoped_session, since: datetime) -> List["Playdates"]:
|
||||
"""Return a list of Playdates objects since passed time"""
|
||||
|
||||
return (
|
||||
@ -203,7 +201,7 @@ class Playlists(Base):
|
||||
"PlaylistRows",
|
||||
back_populates="playlist",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="PlaylistRows.plr_rownum"
|
||||
order_by="PlaylistRows.plr_rownum",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@ -232,11 +230,9 @@ class Playlists(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_playlist_from_template(cls,
|
||||
session: scoped_session,
|
||||
template: "Playlists",
|
||||
playlist_name: str) \
|
||||
-> Optional["Playlists"]:
|
||||
def create_playlist_from_template(
|
||||
cls, session: scoped_session, template: "Playlists", playlist_name: str
|
||||
) -> Optional["Playlists"]:
|
||||
"""Create a new playlist from template"""
|
||||
|
||||
playlist = cls(session, playlist_name)
|
||||
@ -277,9 +273,7 @@ class Playlists(Base):
|
||||
|
||||
return (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.filter(cls.is_template.is_(True))
|
||||
.order_by(cls.name)
|
||||
select(cls).filter(cls.is_template.is_(True)).order_by(cls.name)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
@ -295,7 +289,7 @@ class Playlists(Base):
|
||||
.filter(
|
||||
cls.tab.is_(None),
|
||||
cls.is_template.is_(False),
|
||||
cls.deleted.is_(False)
|
||||
cls.deleted.is_(False),
|
||||
)
|
||||
.order_by(cls.last_used.desc())
|
||||
)
|
||||
@ -310,11 +304,7 @@ class Playlists(Base):
|
||||
"""
|
||||
|
||||
return (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(cls.tab.is_not(None))
|
||||
.order_by(cls.tab)
|
||||
)
|
||||
session.execute(select(cls).where(cls.tab.is_not(None)).order_by(cls.tab))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
@ -329,15 +319,9 @@ class Playlists(Base):
|
||||
def move_tab(session: scoped_session, frm: int, to: int) -> None:
|
||||
"""Move tabs"""
|
||||
|
||||
row_frm = session.execute(
|
||||
select(Playlists)
|
||||
.filter_by(tab=frm)
|
||||
).scalar_one()
|
||||
row_frm = session.execute(select(Playlists).filter_by(tab=frm)).scalar_one()
|
||||
|
||||
row_to = session.execute(
|
||||
select(Playlists)
|
||||
.filter_by(tab=to)
|
||||
).scalar_one()
|
||||
row_to = session.execute(select(Playlists).filter_by(tab=to)).scalar_one()
|
||||
|
||||
row_frm.tab = None
|
||||
row_to.tab = None
|
||||
@ -354,8 +338,9 @@ class Playlists(Base):
|
||||
session.flush()
|
||||
|
||||
@staticmethod
|
||||
def save_as_template(session: scoped_session,
|
||||
playlist_id: int, template_name: str) -> None:
|
||||
def save_as_template(
|
||||
session: scoped_session, playlist_id: int, template_name: str
|
||||
) -> None:
|
||||
"""Save passed playlist as new template"""
|
||||
|
||||
template = Playlists(session, template_name)
|
||||
@ -369,15 +354,14 @@ class Playlists(Base):
|
||||
|
||||
|
||||
class PlaylistRows(Base):
|
||||
__tablename__ = 'playlist_rows'
|
||||
__tablename__ = "playlist_rows"
|
||||
|
||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||
plr_rownum: int = Column(Integer, nullable=False)
|
||||
note: str = Column(String(2048), index=False, default="", nullable=False)
|
||||
playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
|
||||
nullable=False)
|
||||
playlist_id: int = Column(Integer, ForeignKey("playlists.id"), nullable=False)
|
||||
playlist: Playlists = relationship(Playlists, back_populates="rows")
|
||||
track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True)
|
||||
track_id = Column(Integer, ForeignKey("tracks.id"), nullable=True)
|
||||
track: "Tracks" = relationship("Tracks", back_populates="playlistrows")
|
||||
played: bool = Column(Boolean, nullable=False, index=False, default=False)
|
||||
|
||||
@ -388,12 +372,13 @@ class PlaylistRows(Base):
|
||||
f"note={self.note}, plr_rownum={self.plr_rownum}>"
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
playlist_id: int,
|
||||
track_id: Optional[int],
|
||||
row_number: int,
|
||||
note: str = ""
|
||||
note: str = "",
|
||||
) -> None:
|
||||
"""Create PlaylistRows object"""
|
||||
|
||||
@ -409,38 +394,38 @@ class PlaylistRows(Base):
|
||||
|
||||
current_note = self.note
|
||||
if current_note:
|
||||
self.note = current_note + '\n' + extra_note
|
||||
self.note = current_note + "\n" + extra_note
|
||||
else:
|
||||
self.note = extra_note
|
||||
|
||||
@staticmethod
|
||||
def copy_playlist(session: scoped_session,
|
||||
src_id: int,
|
||||
dst_id: int) -> None:
|
||||
def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None:
|
||||
"""Copy playlist entries"""
|
||||
|
||||
src_rows = session.execute(
|
||||
select(PlaylistRows)
|
||||
.filter(PlaylistRows.playlist_id == src_id)
|
||||
).scalars().all()
|
||||
src_rows = (
|
||||
session.execute(
|
||||
select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
for plr in src_rows:
|
||||
PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum,
|
||||
plr.note)
|
||||
PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, plr.note)
|
||||
|
||||
@staticmethod
|
||||
def delete_higher_rows(
|
||||
session: scoped_session, playlist_id: int, maxrow: int) -> None:
|
||||
session: scoped_session, playlist_id: int, maxrow: int
|
||||
) -> None:
|
||||
"""
|
||||
Delete rows in given playlist that have a higher row number
|
||||
than 'maxrow'
|
||||
"""
|
||||
|
||||
session.execute(
|
||||
delete(PlaylistRows)
|
||||
.where(
|
||||
delete(PlaylistRows).where(
|
||||
PlaylistRows.playlist_id == playlist_id,
|
||||
PlaylistRows.plr_rownum > maxrow
|
||||
PlaylistRows.plr_rownum > maxrow,
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
@ -451,11 +436,15 @@ class PlaylistRows(Base):
|
||||
Ensure the row numbers for passed playlist have no gaps
|
||||
"""
|
||||
|
||||
plrs = session.execute(
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(PlaylistRows)
|
||||
.where(PlaylistRows.playlist_id == playlist_id)
|
||||
.order_by(PlaylistRows.plr_rownum)
|
||||
).scalars().all()
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
for i, plr in enumerate(plrs):
|
||||
plr.plr_rownum = i
|
||||
@ -464,113 +453,126 @@ class PlaylistRows(Base):
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_from_id_list(cls, session: scoped_session, playlist_id: int,
|
||||
plr_ids: List[int]) -> List["PlaylistRows"]:
|
||||
def get_from_id_list(
|
||||
cls, session: scoped_session, playlist_id: int, plr_ids: List[int]
|
||||
) -> List["PlaylistRows"]:
|
||||
"""
|
||||
Take a list of PlaylistRows ids and return a list of corresponding
|
||||
PlaylistRows objects
|
||||
"""
|
||||
|
||||
plrs = session.execute(
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.id.in_(plr_ids)
|
||||
).order_by(cls.plr_rownum)).scalars().all()
|
||||
.where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
|
||||
.order_by(cls.plr_rownum)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return plrs
|
||||
|
||||
@staticmethod
|
||||
def get_last_used_row(session: scoped_session,
|
||||
playlist_id: int) -> Optional[int]:
|
||||
def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]:
|
||||
"""Return the last used row for playlist, or None if no rows"""
|
||||
|
||||
return session.execute(
|
||||
select(func.max(PlaylistRows.plr_rownum))
|
||||
.where(PlaylistRows.playlist_id == playlist_id)
|
||||
select(func.max(PlaylistRows.plr_rownum)).where(
|
||||
PlaylistRows.playlist_id == playlist_id
|
||||
)
|
||||
).scalar_one()
|
||||
|
||||
@staticmethod
|
||||
def get_track_plr(session: scoped_session, track_id: int,
|
||||
playlist_id: int) -> Optional["PlaylistRows"]:
|
||||
def get_track_plr(
|
||||
session: scoped_session, track_id: int, playlist_id: int
|
||||
) -> Optional["PlaylistRows"]:
|
||||
"""Return first matching PlaylistRows object or None"""
|
||||
|
||||
return session.scalars(
|
||||
select(PlaylistRows)
|
||||
.where(
|
||||
PlaylistRows.track_id == track_id,
|
||||
PlaylistRows.playlist_id == playlist_id
|
||||
PlaylistRows.playlist_id == playlist_id,
|
||||
)
|
||||
.limit(1)
|
||||
).first()
|
||||
|
||||
@classmethod
|
||||
def get_played_rows(cls, session: scoped_session,
|
||||
playlist_id: int) -> List["PlaylistRows"]:
|
||||
def get_played_rows(
|
||||
cls, session: scoped_session, playlist_id: int
|
||||
) -> List["PlaylistRows"]:
|
||||
"""
|
||||
For passed playlist, return a list of rows that
|
||||
have been played.
|
||||
"""
|
||||
|
||||
plrs = session.execute(
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.played.is_(True)
|
||||
)
|
||||
.where(cls.playlist_id == playlist_id, cls.played.is_(True))
|
||||
.order_by(cls.plr_rownum)
|
||||
).scalars().all()
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return plrs
|
||||
|
||||
@classmethod
|
||||
def get_rows_with_tracks(
|
||||
cls, session: scoped_session, playlist_id: int,
|
||||
cls,
|
||||
session: scoped_session,
|
||||
playlist_id: int,
|
||||
from_row: Optional[int] = None,
|
||||
to_row: Optional[int] = None) -> List["PlaylistRows"]:
|
||||
to_row: Optional[int] = None,
|
||||
) -> List["PlaylistRows"]:
|
||||
"""
|
||||
For passed playlist, return a list of rows that
|
||||
contain tracks
|
||||
"""
|
||||
|
||||
query = select(cls).where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.track_id.is_not(None)
|
||||
cls.playlist_id == playlist_id, cls.track_id.is_not(None)
|
||||
)
|
||||
if from_row is not None:
|
||||
query = query.where(cls.plr_rownum >= from_row)
|
||||
if to_row is not None:
|
||||
query = query.where(cls.plr_rownum <= to_row)
|
||||
|
||||
plrs = (
|
||||
session.execute((query).order_by(cls.plr_rownum)).scalars().all()
|
||||
)
|
||||
plrs = session.execute((query).order_by(cls.plr_rownum)).scalars().all()
|
||||
|
||||
return plrs
|
||||
|
||||
@classmethod
|
||||
def get_unplayed_rows(cls, session: scoped_session,
|
||||
playlist_id: int) -> List["PlaylistRows"]:
|
||||
def get_unplayed_rows(
|
||||
cls, session: scoped_session, playlist_id: int
|
||||
) -> List["PlaylistRows"]:
|
||||
"""
|
||||
For passed playlist, return a list of playlist rows that
|
||||
have not been played.
|
||||
"""
|
||||
|
||||
plrs = session.execute(
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.track_id.is_not(None),
|
||||
cls.played.is_(False)
|
||||
cls.played.is_(False),
|
||||
)
|
||||
.order_by(cls.plr_rownum)
|
||||
).scalars().all()
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return plrs
|
||||
|
||||
@staticmethod
|
||||
def move_rows_down(session: scoped_session, playlist_id: int,
|
||||
starting_row: int, move_by: int) -> None:
|
||||
def move_rows_down(
|
||||
session: scoped_session, playlist_id: int, starting_row: int, move_by: int
|
||||
) -> None:
|
||||
"""
|
||||
Create space to insert move_by additional rows by incremented row
|
||||
number from starting_row to end of playlist
|
||||
@ -580,7 +582,7 @@ class PlaylistRows(Base):
|
||||
update(PlaylistRows)
|
||||
.where(
|
||||
(PlaylistRows.playlist_id == playlist_id),
|
||||
(PlaylistRows.plr_rownum >= starting_row)
|
||||
(PlaylistRows.plr_rownum >= starting_row),
|
||||
)
|
||||
.values(plr_rownum=PlaylistRows.plr_rownum + move_by)
|
||||
)
|
||||
@ -589,7 +591,7 @@ class PlaylistRows(Base):
|
||||
class Settings(Base):
|
||||
"""Manage settings"""
|
||||
|
||||
__tablename__ = 'settings'
|
||||
__tablename__ = "settings"
|
||||
|
||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name: str = Column(String(64), nullable=False, unique=True)
|
||||
@ -602,21 +604,16 @@ class Settings(Base):
|
||||
return f"<Settings(id={self.id}, name={self.name}, {value=}>"
|
||||
|
||||
def __init__(self, session: scoped_session, name: str):
|
||||
|
||||
self.name = name
|
||||
session.add(self)
|
||||
session.flush()
|
||||
|
||||
@classmethod
|
||||
def get_int_settings(cls, session: scoped_session,
|
||||
name: str) -> "Settings":
|
||||
def get_int_settings(cls, session: scoped_session, name: str) -> "Settings":
|
||||
"""Get setting for an integer or return new setting record"""
|
||||
|
||||
try:
|
||||
return session.execute(
|
||||
select(cls)
|
||||
.where(cls.name == name)
|
||||
).scalar_one()
|
||||
return session.execute(select(cls).where(cls.name == name)).scalar_one()
|
||||
|
||||
except NoResultFound:
|
||||
return Settings(session, name)
|
||||
@ -629,7 +626,7 @@ class Settings(Base):
|
||||
|
||||
|
||||
class Tracks(Base):
|
||||
__tablename__ = 'tracks'
|
||||
__tablename__ = "tracks"
|
||||
|
||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||
title = Column(String(256), index=True)
|
||||
@ -641,8 +638,7 @@ class Tracks(Base):
|
||||
path: str = Column(String(2048), index=False, nullable=False, unique=True)
|
||||
mtime = Column(Float, index=True)
|
||||
bitrate = Column(Integer, nullable=True, default=None)
|
||||
playlistrows: PlaylistRows = relationship("PlaylistRows",
|
||||
back_populates="track")
|
||||
playlistrows: PlaylistRows = relationship("PlaylistRows", back_populates="track")
|
||||
playlists = association_proxy("playlistrows", "playlist")
|
||||
playdates: Playdates = relationship("Playdates", back_populates="track")
|
||||
|
||||
@ -693,46 +689,36 @@ class Tracks(Base):
|
||||
return session.execute(select(cls)).scalars().all()
|
||||
|
||||
@classmethod
|
||||
def get_by_path(cls, session: scoped_session,
|
||||
path: str) -> Optional["Tracks"]:
|
||||
def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]:
|
||||
"""
|
||||
Return track with passed path, or None.
|
||||
"""
|
||||
|
||||
try:
|
||||
return (
|
||||
session.execute(
|
||||
select(Tracks)
|
||||
.where(Tracks.path == path)
|
||||
return session.execute(
|
||||
select(Tracks).where(Tracks.path == path)
|
||||
).scalar_one()
|
||||
)
|
||||
except NoResultFound:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def search_artists(cls, session: scoped_session,
|
||||
text: str) -> List["Tracks"]:
|
||||
def search_artists(cls, session: scoped_session, text: str) -> List["Tracks"]:
|
||||
"""Search case-insenstively for artists containing str"""
|
||||
|
||||
return (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(cls.artist.ilike(f"%{text}%"))
|
||||
.order_by(cls.title)
|
||||
select(cls).where(cls.artist.ilike(f"%{text}%")).order_by(cls.title)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def search_titles(cls, session: scoped_session,
|
||||
text: str) -> List["Tracks"]:
|
||||
def search_titles(cls, session: scoped_session, text: str) -> List["Tracks"]:
|
||||
"""Search case-insenstively for titles containing str"""
|
||||
return (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(cls.title.like(f"{text}%"))
|
||||
.order_by(cls.title)
|
||||
select(cls).where(cls.title.like(f"{text}%")).order_by(cls.title)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# import os
|
||||
import threading
|
||||
import vlc # type: ignore
|
||||
|
||||
#
|
||||
from config import Config
|
||||
from datetime import datetime
|
||||
from helpers import file_is_unreadable
|
||||
from typing import Optional
|
||||
from time import sleep
|
||||
@ -19,7 +19,6 @@ lock = threading.Lock()
|
||||
|
||||
|
||||
class FadeTrack(QRunnable):
|
||||
|
||||
def __init__(self, player: vlc.MediaPlayer) -> None:
|
||||
super().__init__()
|
||||
self.player = player
|
||||
@ -47,8 +46,7 @@ class FadeTrack(QRunnable):
|
||||
|
||||
for i in range(1, steps + 1):
|
||||
measures_to_reduce_by += i
|
||||
volume_factor = 1 - (
|
||||
measures_to_reduce_by / total_measures_count)
|
||||
volume_factor = 1 - (measures_to_reduce_by / total_measures_count)
|
||||
self.player.audio_set_volume(int(original_volume * volume_factor))
|
||||
sleep(sleep_time)
|
||||
|
||||
@ -98,8 +96,7 @@ class Music:
|
||||
return None
|
||||
return self.player.get_position()
|
||||
|
||||
def play(self, path: str,
|
||||
position: Optional[float] = None) -> Optional[int]:
|
||||
def play(self, path: str, position: Optional[float] = None) -> Optional[int]:
|
||||
"""
|
||||
Start playing the track at path.
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import icons_rc
|
||||
from datetime import datetime, timedelta
|
||||
from pygame import mixer
|
||||
from time import sleep
|
||||
@ -61,15 +60,7 @@ from dbconfig import (
|
||||
)
|
||||
import helpers
|
||||
import music
|
||||
from models import (
|
||||
Base,
|
||||
Carts,
|
||||
Playdates,
|
||||
PlaylistRows,
|
||||
Playlists,
|
||||
Settings,
|
||||
Tracks
|
||||
)
|
||||
from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
||||
from config import Config
|
||||
from playlists import PlaylistTab
|
||||
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
|
||||
@ -93,7 +84,7 @@ class CartButton(QPushButton):
|
||||
self.cart_id = cart.id
|
||||
if cart.path and cart.enabled and not cart.duration:
|
||||
tags = helpers.get_tags(cart.path)
|
||||
cart.duration = tags['duration']
|
||||
cart.duration = tags["duration"]
|
||||
self.duration = cart.duration
|
||||
self.path = cart.path
|
||||
self.player = None
|
||||
@ -110,8 +101,9 @@ class CartButton(QPushButton):
|
||||
self.pgb.setTextVisible(False)
|
||||
self.pgb.setVisible(False)
|
||||
palette = self.pgb.palette()
|
||||
palette.setColor(QPalette.ColorRole.Highlight,
|
||||
QColor(Config.COLOUR_CART_PROGRESSBAR))
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Highlight, QColor(Config.COLOUR_CART_PROGRESSBAR)
|
||||
)
|
||||
self.pgb.setPalette(palette)
|
||||
self.pgb.setGeometry(0, 0, self.width(), 10)
|
||||
self.pgb.setMinimum(0)
|
||||
@ -157,16 +149,13 @@ class FadeCurve:
|
||||
|
||||
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
|
||||
# milliseconds before fade starts to silence
|
||||
self.start_ms = max(
|
||||
0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
|
||||
self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
|
||||
self.end_ms = track.silence_at
|
||||
self.audio_segment = audio[self.start_ms:self.end_ms]
|
||||
self.audio_segment = audio[self.start_ms : self.end_ms]
|
||||
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
|
||||
|
||||
# Calculate the factor to map milliseconds of track to array
|
||||
self.ms_to_array_factor = (
|
||||
len(self.graph_array) / (self.end_ms - self.start_ms)
|
||||
)
|
||||
self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
|
||||
|
||||
self.region = None
|
||||
|
||||
@ -192,10 +181,7 @@ class FadeCurve:
|
||||
|
||||
if self.region is None:
|
||||
# Create the region now that we're into fade
|
||||
self.region = pg.LinearRegionItem(
|
||||
[0, 0],
|
||||
bounds=[0, len(self.graph_array)]
|
||||
)
|
||||
self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
|
||||
self.GraphWidget.addItem(self.region)
|
||||
|
||||
# Update region position
|
||||
@ -286,8 +272,9 @@ class PlaylistTrack:
|
||||
f"playlist_id={self.playlist_id}>"
|
||||
)
|
||||
|
||||
def set_plr(self, session: scoped_session, plr: PlaylistRows,
|
||||
tab: PlaylistTab) -> None:
|
||||
def set_plr(
|
||||
self, session: scoped_session, plr: PlaylistRows, tab: PlaylistTab
|
||||
) -> None:
|
||||
"""
|
||||
Update with new plr information
|
||||
"""
|
||||
@ -323,8 +310,7 @@ class PlaylistTrack:
|
||||
|
||||
self.start_time = datetime.now()
|
||||
if self.duration:
|
||||
self.end_time = (
|
||||
self.start_time + timedelta(milliseconds=self.duration))
|
||||
self.end_time = self.start_time + timedelta(milliseconds=self.duration)
|
||||
|
||||
|
||||
class Window(QMainWindow, Ui_MainWindow):
|
||||
@ -355,14 +341,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.txtSearch.setHidden(True)
|
||||
self.hide_played_tracks = False
|
||||
mixer.init()
|
||||
self.widgetFadeVolume.hideAxis('bottom')
|
||||
self.widgetFadeVolume.hideAxis('left')
|
||||
self.widgetFadeVolume.hideAxis("bottom")
|
||||
self.widgetFadeVolume.hideAxis("left")
|
||||
self.widgetFadeVolume.setDefaultPadding(0)
|
||||
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
|
||||
FadeCurve.GraphWidget = self.widgetFadeVolume
|
||||
|
||||
self.visible_playlist_tab: Callable[[], PlaylistTab] = \
|
||||
self.tabPlaylist.currentWidget
|
||||
self.visible_playlist_tab: Callable[
|
||||
[], PlaylistTab
|
||||
] = self.tabPlaylist.currentWidget
|
||||
|
||||
self.load_last_playlists()
|
||||
if Config.CARTS_HIDE:
|
||||
@ -380,10 +367,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
try:
|
||||
git_tag = str(
|
||||
subprocess.check_output(
|
||||
['git', 'describe'], stderr=subprocess.STDOUT
|
||||
)
|
||||
).strip('\'b\\n')
|
||||
subprocess.check_output(["git", "describe"], stderr=subprocess.STDOUT)
|
||||
).strip("'b\\n")
|
||||
except subprocess.CalledProcessError as exc_info:
|
||||
git_tag = str(exc_info.output)
|
||||
|
||||
@ -395,7 +380,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self,
|
||||
"About",
|
||||
f"MusicMuster {git_tag}\n\nDatabase: {dbname}",
|
||||
QMessageBox.StandardButton.Ok
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def cart_configure(self, cart: Carts, btn: CartButton) -> None:
|
||||
@ -433,15 +418,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# Don't allow clicks while we're playing
|
||||
btn.setEnabled(False)
|
||||
if not btn.player:
|
||||
log.debug(
|
||||
f"musicmuster.cart_click(): no player assigned ({btn=})")
|
||||
log.debug(f"musicmuster.cart_click(): no player assigned ({btn=})")
|
||||
return
|
||||
|
||||
btn.player.play()
|
||||
btn.is_playing = True
|
||||
colour = Config.COLOUR_CART_PLAYING
|
||||
thread = threading.Thread(target=self.cart_progressbar,
|
||||
args=(btn,))
|
||||
thread = threading.Thread(target=self.cart_progressbar, args=(btn,))
|
||||
thread.start()
|
||||
else:
|
||||
colour = Config.COLOUR_CART_ERROR
|
||||
@ -469,7 +452,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
if cart.path and not helpers.file_is_unreadable(cart.path):
|
||||
tags = helpers.get_tags(cart.path)
|
||||
cart.duration = tags['duration']
|
||||
cart.duration = tags["duration"]
|
||||
|
||||
cart.enabled = dlg.ui.chkEnabled.isChecked()
|
||||
cart.name = name
|
||||
@ -488,8 +471,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
for cart_number in range(1, Config.CARTS_COUNT + 1):
|
||||
cart = session.query(Carts).get(cart_number)
|
||||
if cart is None:
|
||||
cart = Carts(session, cart_number,
|
||||
name=f"Cart #{cart_number}")
|
||||
cart = Carts(session, cart_number, name=f"Cart #{cart_number}")
|
||||
|
||||
btn = CartButton(self, cart)
|
||||
btn.clicked.connect(self.cart_click)
|
||||
@ -540,7 +522,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.update_headers()
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
""" Clear selected row"""
|
||||
"""Clear selected row"""
|
||||
|
||||
# Unselect any selected rows
|
||||
if self.visible_playlist_tab():
|
||||
@ -555,27 +537,25 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if self.playing:
|
||||
event.ignore()
|
||||
helpers.show_warning(
|
||||
self,
|
||||
"Track playing",
|
||||
"Can't close application while track is playing")
|
||||
self, "Track playing", "Can't close application while track is playing"
|
||||
)
|
||||
else:
|
||||
with Session() as session:
|
||||
record = Settings.get_int_settings(
|
||||
session, "mainwindow_height")
|
||||
record = Settings.get_int_settings(session, "mainwindow_height")
|
||||
if record.f_int != self.height():
|
||||
record.update(session, {'f_int': self.height()})
|
||||
record.update(session, {"f_int": self.height()})
|
||||
|
||||
record = Settings.get_int_settings(session, "mainwindow_width")
|
||||
if record.f_int != self.width():
|
||||
record.update(session, {'f_int': self.width()})
|
||||
record.update(session, {"f_int": self.width()})
|
||||
|
||||
record = Settings.get_int_settings(session, "mainwindow_x")
|
||||
if record.f_int != self.x():
|
||||
record.update(session, {'f_int': self.x()})
|
||||
record.update(session, {"f_int": self.x()})
|
||||
|
||||
record = Settings.get_int_settings(session, "mainwindow_y")
|
||||
if record.f_int != self.y():
|
||||
record.update(session, {'f_int': self.y()})
|
||||
record.update(session, {"f_int": self.y()})
|
||||
|
||||
# Save splitter settings
|
||||
splitter_sizes = self.splitter.sizes()
|
||||
@ -584,16 +564,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
record = Settings.get_int_settings(session, "splitter_top")
|
||||
if record.f_int != splitter_top:
|
||||
record.update(session, {'f_int': splitter_top})
|
||||
record.update(session, {"f_int": splitter_top})
|
||||
|
||||
record = Settings.get_int_settings(session, "splitter_bottom")
|
||||
if record.f_int != splitter_bottom:
|
||||
record.update(session, {'f_int': splitter_bottom})
|
||||
record.update(session, {"f_int": splitter_bottom})
|
||||
|
||||
# Save current tab
|
||||
record = Settings.get_int_settings(session, "active_tab")
|
||||
record.update(session,
|
||||
{'f_int': self.tabPlaylist.currentIndex()})
|
||||
record.update(session, {"f_int": self.tabPlaylist.currentIndex()})
|
||||
|
||||
event.accept()
|
||||
|
||||
@ -613,10 +592,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
# Don't close current track playlist
|
||||
if self.tabPlaylist.widget(tab_index) == (
|
||||
self.current_track.playlist_tab):
|
||||
self.statusbar.showMessage(
|
||||
"Can't close current track playlist", 5000)
|
||||
if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
|
||||
self.statusbar.showMessage("Can't close current track playlist", 5000)
|
||||
return False
|
||||
|
||||
# Attempt to close next track playlist
|
||||
@ -643,15 +620,17 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
|
||||
self.actionDeletePlaylist.triggered.connect(self.delete_playlist)
|
||||
self.actionDownload_CSV_of_played_tracks.triggered.connect(
|
||||
self.download_played_tracks)
|
||||
self.actionEnable_controls.triggered.connect(
|
||||
self.enable_play_next_controls)
|
||||
self.download_played_tracks
|
||||
)
|
||||
self.actionEnable_controls.triggered.connect(self.enable_play_next_controls)
|
||||
self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
|
||||
self.actionFade.triggered.connect(self.fade)
|
||||
self.actionFind_next.triggered.connect(
|
||||
lambda: self.tabPlaylist.currentWidget().search_next())
|
||||
lambda: self.tabPlaylist.currentWidget().search_next()
|
||||
)
|
||||
self.actionFind_previous.triggered.connect(
|
||||
lambda: self.tabPlaylist.currentWidget().search_previous())
|
||||
lambda: self.tabPlaylist.currentWidget().search_previous()
|
||||
)
|
||||
self.actionImport.triggered.connect(self.import_track)
|
||||
self.actionInsertSectionHeader.triggered.connect(self.insert_header)
|
||||
self.actionInsertTrack.triggered.connect(self.insert_track)
|
||||
@ -666,13 +645,14 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.actionResume.triggered.connect(self.resume)
|
||||
self.actionSave_as_template.triggered.connect(self.save_as_template)
|
||||
self.actionSearch_title_in_Songfacts.triggered.connect(
|
||||
lambda: self.tabPlaylist.currentWidget().lookup_row_in_songfacts())
|
||||
lambda: self.tabPlaylist.currentWidget().lookup_row_in_songfacts()
|
||||
)
|
||||
self.actionSearch_title_in_Wikipedia.triggered.connect(
|
||||
lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia())
|
||||
lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia()
|
||||
)
|
||||
self.actionSearch.triggered.connect(self.search_playlist)
|
||||
self.actionSelect_next_track.triggered.connect(self.select_next_row)
|
||||
self.actionSelect_previous_track.triggered.connect(
|
||||
self.select_previous_row)
|
||||
self.actionSelect_previous_track.triggered.connect(self.select_previous_row)
|
||||
self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
|
||||
self.actionSetNext.triggered.connect(self.set_selected_track_next)
|
||||
self.actionSkipToNext.triggered.connect(self.play_next)
|
||||
@ -691,10 +671,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.timer.timeout.connect(self.tick)
|
||||
|
||||
def create_playlist(self,
|
||||
session: scoped_session,
|
||||
playlist_name: Optional[str] = None) \
|
||||
-> Optional[Playlists]:
|
||||
def create_playlist(
|
||||
self, session: scoped_session, playlist_name: Optional[str] = None
|
||||
) -> Optional[Playlists]:
|
||||
"""Create new playlist"""
|
||||
|
||||
playlist_name = self.solicit_playlist_name()
|
||||
@ -712,8 +691,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if playlist:
|
||||
self.create_playlist_tab(session, playlist)
|
||||
|
||||
def create_playlist_tab(self, session: scoped_session,
|
||||
playlist: Playlists) -> int:
|
||||
def create_playlist_tab(self, session: scoped_session, playlist: Playlists) -> int:
|
||||
"""
|
||||
Take the passed playlist database object, create a playlist tab and
|
||||
add tab to display. Return index number of tab.
|
||||
@ -722,8 +700,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
assert playlist.id
|
||||
|
||||
playlist_tab = PlaylistTab(
|
||||
musicmuster=self, session=session, playlist_id=playlist.id,
|
||||
signals=self.signals)
|
||||
musicmuster=self,
|
||||
session=session,
|
||||
playlist_id=playlist.id,
|
||||
signals=self.signals,
|
||||
)
|
||||
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||
self.tabPlaylist.setCurrentIndex(idx)
|
||||
|
||||
@ -737,8 +718,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
with Session() as session:
|
||||
# Save the selected PlaylistRows items ready for a later
|
||||
# paste
|
||||
self.selected_plrs = (
|
||||
self.visible_playlist_tab().get_selected_playlistrows(session))
|
||||
self.selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
|
||||
session
|
||||
)
|
||||
|
||||
def debug(self):
|
||||
"""Invoke debugger"""
|
||||
@ -746,6 +728,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
visible_playlist_id = self.visible_playlist_tab().playlist_id
|
||||
print(f"Active playlist id={visible_playlist_id}")
|
||||
import ipdb # type: ignore
|
||||
|
||||
ipdb.set_trace()
|
||||
|
||||
def delete_playlist(self) -> None:
|
||||
@ -757,9 +740,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
playlist_id = self.visible_playlist_tab().playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
if helpers.ask_yes_no("Delete playlist",
|
||||
f"Delete playlist '{playlist.name}': "
|
||||
"Are you sure?"
|
||||
if helpers.ask_yes_no(
|
||||
"Delete playlist",
|
||||
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
||||
):
|
||||
if self.close_playlist_tab():
|
||||
playlist.delete(session)
|
||||
@ -780,9 +763,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime()
|
||||
# Get output filename
|
||||
pathspec = QFileDialog.getSaveFileName(
|
||||
self, 'Save CSV of tracks played',
|
||||
self,
|
||||
"Save CSV of tracks played",
|
||||
directory="/tmp/playlist.csv",
|
||||
filter="CSV files (*.csv)"
|
||||
filter="CSV files (*.csv)",
|
||||
)
|
||||
if not pathspec:
|
||||
return
|
||||
@ -794,9 +778,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
with open(path, "w") as f:
|
||||
with Session() as session:
|
||||
for playdate in Playdates.played_after(session, start_dt):
|
||||
f.write(
|
||||
f"{playdate.track.artist},{playdate.track.title}\n"
|
||||
)
|
||||
f.write(f"{playdate.track.artist},{playdate.track.title}\n")
|
||||
|
||||
def drop3db(self) -> None:
|
||||
"""Drop music level by 3db if button checked"""
|
||||
@ -867,16 +849,16 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
playlist_id = self.visible_playlist_tab().playlist_id
|
||||
|
||||
with Session() as session:
|
||||
|
||||
# Get output filename
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if not playlist:
|
||||
return
|
||||
|
||||
pathspec = QFileDialog.getSaveFileName(
|
||||
self, 'Save Playlist',
|
||||
self,
|
||||
"Save Playlist",
|
||||
directory=f"{playlist.name}.m3u",
|
||||
filter="M3U files (*.m3u);;All files (*.*)"
|
||||
filter="M3U files (*.m3u);;All files (*.*)",
|
||||
)
|
||||
if not pathspec:
|
||||
return
|
||||
@ -923,10 +905,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
times a second; this function has much better resolution.
|
||||
"""
|
||||
|
||||
if (
|
||||
self.current_track.track_id is None or
|
||||
self.current_track.start_time is None
|
||||
):
|
||||
if self.current_track.track_id is None or self.current_track.start_time is None:
|
||||
return 0
|
||||
|
||||
now = datetime.now()
|
||||
@ -965,16 +944,16 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
txt = ""
|
||||
tags = helpers.get_tags(fname)
|
||||
new_tracks.append(fname)
|
||||
title = tags['title']
|
||||
artist = tags['artist']
|
||||
title = tags["title"]
|
||||
artist = tags["artist"]
|
||||
count = 0
|
||||
possible_matches = Tracks.search_titles(session, title)
|
||||
if possible_matches:
|
||||
txt += 'Similar to new track '
|
||||
txt += "Similar to new track "
|
||||
txt += f'"{title}" by "{artist} ({fname})":\n\n'
|
||||
for track in possible_matches:
|
||||
txt += f' "{track.title}" by {track.artist}'
|
||||
txt += f' ({track.path})\n\n'
|
||||
txt += f" ({track.path})\n\n"
|
||||
count += 1
|
||||
if count >= Config.MAX_IMPORT_MATCHES:
|
||||
txt += "\nThere are more similar-looking tracks"
|
||||
@ -987,7 +966,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"Possible duplicates",
|
||||
txt,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
QMessageBox.StandardButton.Cancel
|
||||
QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if result == QMessageBox.StandardButton.Cancel:
|
||||
return
|
||||
@ -1005,9 +984,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self, "Import error", "Error importing " + msg
|
||||
)
|
||||
)
|
||||
self.worker.importing.connect(
|
||||
lambda msg: self.statusbar.showMessage(msg, 5000)
|
||||
)
|
||||
self.worker.importing.connect(lambda msg: self.statusbar.showMessage(msg, 5000))
|
||||
self.worker.finished.connect(self.import_complete)
|
||||
self.import_thread.start()
|
||||
|
||||
@ -1057,8 +1034,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if record and record.f_int >= 0:
|
||||
self.tabPlaylist.setCurrentIndex(record.f_int)
|
||||
|
||||
def move_playlist_rows(self, session: scoped_session,
|
||||
playlistrows: List[PlaylistRows]) -> None:
|
||||
def move_playlist_rows(
|
||||
self, session: scoped_session, playlistrows: List[PlaylistRows]
|
||||
) -> None:
|
||||
"""
|
||||
Move passed playlist rows to another playlist
|
||||
|
||||
@ -1071,14 +1049,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
# Remove current/next rows from list
|
||||
plrs_to_move = [plr for plr in playlistrows if
|
||||
plr.id not in
|
||||
[self.current_track.plr_id,
|
||||
self.next_track.plr_id]
|
||||
plrs_to_move = [
|
||||
plr
|
||||
for plr in playlistrows
|
||||
if plr.id not in [self.current_track.plr_id, self.next_track.plr_id]
|
||||
]
|
||||
|
||||
rows_to_delete = [plr.plr_rownum for plr in plrs_to_move
|
||||
if plr.plr_rownum is not None]
|
||||
rows_to_delete = [
|
||||
plr.plr_rownum for plr in plrs_to_move if plr.plr_rownum is not None
|
||||
]
|
||||
if not rows_to_delete:
|
||||
return
|
||||
|
||||
@ -1100,8 +1079,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
destination_playlist_id = dlg.playlist.id
|
||||
|
||||
# Update destination playlist in the database
|
||||
last_row = PlaylistRows.get_last_used_row(session,
|
||||
destination_playlist_id)
|
||||
last_row = PlaylistRows.get_last_used_row(session, destination_playlist_id)
|
||||
if last_row is not None:
|
||||
next_row = last_row + 1
|
||||
else:
|
||||
@ -1135,8 +1113,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
selected_plrs = (
|
||||
self.visible_playlist_tab().get_selected_playlistrows(session))
|
||||
selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
|
||||
session
|
||||
)
|
||||
if not selected_plrs:
|
||||
return
|
||||
|
||||
@ -1155,11 +1134,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
playlist_id = self.visible_playlist_tab().playlist_id
|
||||
with Session() as session:
|
||||
unplayed_plrs = PlaylistRows.get_unplayed_rows(
|
||||
session, playlist_id)
|
||||
if helpers.ask_yes_no("Move tracks",
|
||||
f"Move {len(unplayed_plrs)} tracks:"
|
||||
" Are you sure?"
|
||||
unplayed_plrs = PlaylistRows.get_unplayed_rows(session, playlist_id)
|
||||
if helpers.ask_yes_no(
|
||||
"Move tracks", f"Move {len(unplayed_plrs)} tracks:" " Are you sure?"
|
||||
):
|
||||
self.move_playlist_rows(session, unplayed_plrs)
|
||||
|
||||
@ -1168,8 +1145,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
with Session() as session:
|
||||
templates = Playlists.get_all_templates(session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=templates,
|
||||
session=session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=templates, session=session)
|
||||
dlg.exec()
|
||||
template = dlg.playlist
|
||||
if template:
|
||||
@ -1177,7 +1153,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if not playlist_name:
|
||||
return
|
||||
playlist = Playlists.create_playlist_from_template(
|
||||
session, template, playlist_name)
|
||||
session, template, playlist_name
|
||||
)
|
||||
if not playlist:
|
||||
return
|
||||
tab_index = self.create_playlist_tab(session, playlist)
|
||||
@ -1188,8 +1165,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
with Session() as session:
|
||||
playlists = Playlists.get_closed(session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists,
|
||||
session=session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||
dlg.exec()
|
||||
playlist = dlg.playlist
|
||||
if playlist:
|
||||
@ -1217,8 +1193,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
with Session() as session:
|
||||
# Create space in destination playlist
|
||||
PlaylistRows.move_rows_down(session, dst_playlist_id,
|
||||
dst_row, len(self.selected_plrs))
|
||||
PlaylistRows.move_rows_down(
|
||||
session, dst_playlist_id, dst_row, len(self.selected_plrs)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
# Update plrs
|
||||
@ -1240,7 +1217,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# Update display
|
||||
self.visible_playlist_tab().populate_display(
|
||||
session, dst_playlist_id, scroll_to_top=False)
|
||||
session, dst_playlist_id, scroll_to_top=False
|
||||
)
|
||||
|
||||
# If source playlist is not destination playlist, fixup row
|
||||
# numbers and update display
|
||||
@ -1250,13 +1228,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# will be re-populated when it is opened)
|
||||
source_playlist_tab = None
|
||||
for tab in range(self.tabPlaylist.count()):
|
||||
if self.tabPlaylist.widget(tab).playlist_id == \
|
||||
src_playlist_id:
|
||||
if self.tabPlaylist.widget(tab).playlist_id == src_playlist_id:
|
||||
source_playlist_tab = self.tabPlaylist.widget(tab)
|
||||
break
|
||||
if source_playlist_tab:
|
||||
source_playlist_tab.populate_display(
|
||||
session, src_playlist_id, scroll_to_top=False)
|
||||
session, src_playlist_id, scroll_to_top=False
|
||||
)
|
||||
|
||||
# Reset so rows can't be repasted
|
||||
self.selected_plrs = None
|
||||
@ -1305,8 +1283,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# Set current track playlist_tab colour
|
||||
current_tab = self.current_track.playlist_tab
|
||||
if current_tab:
|
||||
self.set_tab_colour(
|
||||
current_tab, QColor(Config.COLOUR_CURRENT_TAB))
|
||||
self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
|
||||
|
||||
# Restore volume if -3dB active
|
||||
if self.btnDrop3db.isChecked():
|
||||
@ -1356,8 +1333,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
if self.btnPreview.isChecked():
|
||||
# Get track path for first selected track if there is one
|
||||
track_path = (
|
||||
self.visible_playlist_tab().get_selected_row_track_path())
|
||||
track_path = self.visible_playlist_tab().get_selected_row_track_path()
|
||||
if not track_path:
|
||||
# Otherwise get path to next track to play
|
||||
track_path = self.next_track.path
|
||||
@ -1405,7 +1381,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if self.current_track.track_id:
|
||||
playing_track = self.current_track
|
||||
|
||||
with Session() as session:
|
||||
# Set next plr to be track to resume
|
||||
if not self.previous_track.plr_id:
|
||||
return
|
||||
@ -1413,8 +1388,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
|
||||
# Resume last track
|
||||
self.set_next_plr_id(self.previous_track.plr_id,
|
||||
self.previous_track.playlist_tab)
|
||||
self.set_next_plr_id(
|
||||
self.previous_track.plr_id, self.previous_track.playlist_tab
|
||||
)
|
||||
self.play_next(self.previous_track_position)
|
||||
|
||||
# If a track was playing when we were called, get details to
|
||||
@ -1424,16 +1400,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
if not playing_track.playlist_tab:
|
||||
return
|
||||
self.set_next_plr_id(playing_track.plr_id,
|
||||
playing_track.playlist_tab)
|
||||
self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab)
|
||||
|
||||
def save_as_template(self) -> None:
|
||||
"""Save current playlist as template"""
|
||||
|
||||
with Session() as session:
|
||||
template_names = [
|
||||
a.name for a in Playlists.get_all_templates(session)
|
||||
]
|
||||
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
||||
|
||||
while True:
|
||||
# Get name for new template
|
||||
@ -1448,13 +1421,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
template_name = dlg.textValue()
|
||||
if template_name not in template_names:
|
||||
break
|
||||
helpers.show_warning(self,
|
||||
"Duplicate template",
|
||||
"Template name already in use"
|
||||
helpers.show_warning(
|
||||
self, "Duplicate template", "Template name already in use"
|
||||
)
|
||||
Playlists.save_as_template(
|
||||
session, self.visible_playlist_tab().playlist_id, template_name
|
||||
)
|
||||
Playlists.save_as_template(session,
|
||||
self.visible_playlist_tab().playlist_id,
|
||||
template_name)
|
||||
helpers.show_OK(self, "Template", "Template saved")
|
||||
|
||||
def search_playlist(self) -> None:
|
||||
@ -1535,8 +1507,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
|
||||
self.tabPlaylist.currentWidget().scroll_next_to_top()
|
||||
|
||||
def solicit_playlist_name(self,
|
||||
default: Optional[str] = "") -> Optional[str]:
|
||||
def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]:
|
||||
"""Get name of playlist from user"""
|
||||
|
||||
dlg = QInputDialog(self)
|
||||
@ -1581,11 +1552,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# Reset playlist_tab colour
|
||||
if self.current_track.playlist_tab:
|
||||
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))
|
||||
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))
|
||||
self.set_tab_colour(
|
||||
self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||
)
|
||||
|
||||
# Run end-of-track actions
|
||||
self.end_of_track_actions()
|
||||
@ -1612,8 +1585,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.set_next_plr_id(selected_plr_ids[0], playlist_tab)
|
||||
|
||||
def set_next_plr_id(self, next_plr_id: Optional[int],
|
||||
playlist_tab: PlaylistTab) -> None:
|
||||
def set_next_plr_id(
|
||||
self, next_plr_id: Optional[int], playlist_tab: PlaylistTab
|
||||
) -> None:
|
||||
"""
|
||||
Set passed plr_id as next track to play, or clear next track if None
|
||||
|
||||
@ -1636,8 +1610,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# Tell playlist tabs to update their 'next track' highlighting
|
||||
# Args must both be ints, so use zero for no next track
|
||||
self.signals.set_next_track_signal.emit(old_next_track.plr_id,
|
||||
next_plr_id or 0)
|
||||
self.signals.set_next_track_signal.emit(
|
||||
old_next_track.plr_id, next_plr_id or 0
|
||||
)
|
||||
|
||||
# Update headers
|
||||
self.update_headers()
|
||||
@ -1650,12 +1625,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# because it isn't quick
|
||||
if self.next_track.title:
|
||||
QTimer.singleShot(
|
||||
1, lambda: self.tabInfolist.open_in_wikipedia(
|
||||
self.next_track.title)
|
||||
1,
|
||||
lambda: self.tabInfolist.open_in_wikipedia(
|
||||
self.next_track.title
|
||||
),
|
||||
)
|
||||
|
||||
def _set_next_track_playlist_tab_colours(
|
||||
self, old_next_track: Optional[PlaylistTrack]) -> None:
|
||||
self, old_next_track: Optional[PlaylistTrack]
|
||||
) -> None:
|
||||
"""
|
||||
Set playlist tab colour for next track. self.next_track needs
|
||||
to be set before calling.
|
||||
@ -1664,14 +1642,14 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# If the original next playlist tab isn't the same as the
|
||||
# new one or the current track, it needs its colour reset.
|
||||
if (
|
||||
old_next_track and
|
||||
old_next_track.playlist_tab and
|
||||
old_next_track.playlist_tab not in [
|
||||
self.next_track.playlist_tab,
|
||||
self.current_track.playlist_tab
|
||||
]):
|
||||
self.set_tab_colour(old_next_track.playlist_tab,
|
||||
QColor(Config.COLOUR_NORMAL_TAB))
|
||||
old_next_track
|
||||
and old_next_track.playlist_tab
|
||||
and old_next_track.playlist_tab
|
||||
not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
|
||||
):
|
||||
self.set_tab_colour(
|
||||
old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||
)
|
||||
# If the new next playlist tab isn't the same as the
|
||||
# old one or the current track, it needs its colour set.
|
||||
if old_next_track:
|
||||
@ -1679,14 +1657,14 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
else:
|
||||
old_tab = None
|
||||
if (
|
||||
self.next_track and
|
||||
self.next_track.playlist_tab and
|
||||
self.next_track.playlist_tab not in [
|
||||
old_tab,
|
||||
self.current_track.playlist_tab
|
||||
]):
|
||||
self.set_tab_colour(self.next_track.playlist_tab,
|
||||
QColor(Config.COLOUR_NEXT_TAB))
|
||||
self.next_track
|
||||
and self.next_track.playlist_tab
|
||||
and self.next_track.playlist_tab
|
||||
not in [old_tab, self.current_track.playlist_tab]
|
||||
):
|
||||
self.set_tab_colour(
|
||||
self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
|
||||
)
|
||||
|
||||
def tick(self) -> None:
|
||||
"""
|
||||
@ -1713,9 +1691,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# Update volume fade curve
|
||||
if (
|
||||
self.current_track.track_id and
|
||||
self.current_track.fade_graph and
|
||||
self.current_track.start_time
|
||||
self.current_track.track_id
|
||||
and self.current_track.fade_graph
|
||||
and self.current_track.start_time
|
||||
):
|
||||
play_time = (
|
||||
datetime.now() - self.current_track.start_time
|
||||
@ -1727,8 +1705,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Called every 500ms
|
||||
"""
|
||||
|
||||
self.lblTOD.setText(datetime.now().strftime(
|
||||
Config.TOD_TIME_FORMAT))
|
||||
self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
|
||||
# Update carts
|
||||
self.cart_tick()
|
||||
|
||||
@ -1748,15 +1725,19 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# player.is_playing() returning True, so assume playing if less
|
||||
# than Config.PLAY_SETTLE microseconds have passed since
|
||||
# starting play.
|
||||
if self.music.player and self.current_track.start_time and (
|
||||
self.music.player.is_playing() or
|
||||
(datetime.now() - self.current_track.start_time)
|
||||
< timedelta(microseconds=Config.PLAY_SETTLE)):
|
||||
if (
|
||||
self.music.player
|
||||
and self.current_track.start_time
|
||||
and (
|
||||
self.music.player.is_playing()
|
||||
or (datetime.now() - self.current_track.start_time)
|
||||
< timedelta(microseconds=Config.PLAY_SETTLE)
|
||||
)
|
||||
):
|
||||
playtime = self.get_playtime()
|
||||
time_to_fade = (self.current_track.fade_at - playtime)
|
||||
time_to_silence = (
|
||||
self.current_track.silence_at - playtime)
|
||||
time_to_end = (self.current_track.duration - playtime)
|
||||
time_to_fade = self.current_track.fade_at - playtime
|
||||
time_to_silence = self.current_track.silence_at - playtime
|
||||
time_to_end = self.current_track.duration - playtime
|
||||
|
||||
# Elapsed time
|
||||
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
|
||||
@ -1787,9 +1768,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.frame_silent.setStyleSheet("")
|
||||
self.frame_fade.setStyleSheet("")
|
||||
|
||||
self.label_silent_timer.setText(
|
||||
helpers.ms_to_mmss(time_to_silence)
|
||||
)
|
||||
self.label_silent_timer.setText(helpers.ms_to_mmss(time_to_silence))
|
||||
|
||||
# Time to end
|
||||
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
|
||||
@ -1834,8 +1813,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
class CartDialog(QDialog):
|
||||
"""Edit cart details"""
|
||||
|
||||
def __init__(self, musicmuster: Window, session: scoped_session,
|
||||
cart: Carts, *args, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Manage carts
|
||||
"""
|
||||
@ -1871,8 +1851,14 @@ class CartDialog(QDialog):
|
||||
class DbDialog(QDialog):
|
||||
"""Select track from database"""
|
||||
|
||||
def __init__(self, musicmuster: Window, session: scoped_session,
|
||||
get_one_track: bool = False, *args, **kwargs) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
musicmuster: Window,
|
||||
session: scoped_session,
|
||||
get_one_track: bool = False,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Subclassed QDialog to manage track selection
|
||||
|
||||
@ -1911,17 +1897,16 @@ class DbDialog(QDialog):
|
||||
|
||||
record = Settings.get_int_settings(self.session, "dbdialog_height")
|
||||
if record.f_int != self.height():
|
||||
record.update(self.session, {'f_int': self.height()})
|
||||
record.update(self.session, {"f_int": self.height()})
|
||||
|
||||
record = Settings.get_int_settings(self.session, "dbdialog_width")
|
||||
if record.f_int != self.width():
|
||||
record.update(self.session, {'f_int': self.width()})
|
||||
record.update(self.session, {"f_int": self.width()})
|
||||
|
||||
def add_selected(self) -> None:
|
||||
"""Handle Add button"""
|
||||
|
||||
if (not self.ui.matchList.selectedItems() and
|
||||
not self.ui.txtNote.text()):
|
||||
if not self.ui.matchList.selectedItems() and not self.ui.txtNote.text():
|
||||
return
|
||||
|
||||
track = None
|
||||
@ -1946,10 +1931,12 @@ class DbDialog(QDialog):
|
||||
|
||||
if track:
|
||||
self.musicmuster.visible_playlist_tab().insert_track(
|
||||
self.session, track, note=self.ui.txtNote.text())
|
||||
self.session, track, note=self.ui.txtNote.text()
|
||||
)
|
||||
else:
|
||||
self.musicmuster.visible_playlist_tab().insert_header(
|
||||
self.session, note=self.ui.txtNote.text())
|
||||
self.session, note=self.ui.txtNote.text()
|
||||
)
|
||||
|
||||
# TODO: this shouldn't be needed as insert_track() saves
|
||||
# playlist
|
||||
@ -2041,11 +2028,11 @@ class SelectPlaylistDialog(QDialog):
|
||||
self.session = session
|
||||
self.playlist = None
|
||||
|
||||
record = Settings.get_int_settings(
|
||||
self.session, "select_playlist_dialog_width")
|
||||
record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
|
||||
width = record.f_int or 800
|
||||
record = Settings.get_int_settings(
|
||||
self.session, "select_playlist_dialog_height")
|
||||
self.session, "select_playlist_dialog_height"
|
||||
)
|
||||
height = record.f_int or 600
|
||||
self.resize(width, height)
|
||||
|
||||
@ -2057,14 +2044,14 @@ class SelectPlaylistDialog(QDialog):
|
||||
|
||||
def __del__(self): # review
|
||||
record = Settings.get_int_settings(
|
||||
self.session, "select_playlist_dialog_height")
|
||||
self.session, "select_playlist_dialog_height"
|
||||
)
|
||||
if record.f_int != self.height():
|
||||
record.update(self.session, {'f_int': self.height()})
|
||||
record.update(self.session, {"f_int": self.height()})
|
||||
|
||||
record = Settings.get_int_settings(
|
||||
self.session, "select_playlist_dialog_width")
|
||||
record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
|
||||
if record.f_int != self.width():
|
||||
record.update(self.session, {'f_int': self.width()})
|
||||
record.update(self.session, {"f_int": self.width()})
|
||||
|
||||
def list_doubleclick(self, entry): # review
|
||||
self.playlist = entry.data(Qt.ItemDataRole.UserRole)
|
||||
@ -2086,12 +2073,22 @@ if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser()
|
||||
# Only allow at most one option to be specified
|
||||
group = p.add_mutually_exclusive_group()
|
||||
group.add_argument('-b', '--bitrates',
|
||||
action="store_true", dest="update_bitrates",
|
||||
default=False, help="Update bitrates in database")
|
||||
group.add_argument('-c', '--check-database',
|
||||
action="store_true", dest="check_db",
|
||||
default=False, help="Check and report on database")
|
||||
group.add_argument(
|
||||
"-b",
|
||||
"--bitrates",
|
||||
action="store_true",
|
||||
dest="update_bitrates",
|
||||
default=False,
|
||||
help="Update bitrates in database",
|
||||
)
|
||||
group.add_argument(
|
||||
"-c",
|
||||
"--check-database",
|
||||
action="store_true",
|
||||
dest="check_db",
|
||||
default=False,
|
||||
help="Check and report on database",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
# Run as required
|
||||
@ -2111,13 +2108,16 @@ if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
# PyQt6 defaults to a grey for labels
|
||||
palette = app.palette()
|
||||
palette.setColor(QPalette.ColorRole.WindowText,
|
||||
QColor(Config.COLOUR_LABEL_TEXT))
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.WindowText, QColor(Config.COLOUR_LABEL_TEXT)
|
||||
)
|
||||
# Set colours that will be used by playlist row stripes
|
||||
palette.setColor(QPalette.ColorRole.Base,
|
||||
QColor(Config.COLOUR_EVEN_PLAYLIST))
|
||||
palette.setColor(QPalette.ColorRole.AlternateBase,
|
||||
QColor(Config.COLOUR_ODD_PLAYLIST))
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.Base, QColor(Config.COLOUR_EVEN_PLAYLIST)
|
||||
)
|
||||
palette.setColor(
|
||||
QPalette.ColorRole.AlternateBase, QColor(Config.COLOUR_ODD_PLAYLIST)
|
||||
)
|
||||
app.setPalette(palette)
|
||||
win = Window()
|
||||
win.show()
|
||||
@ -2129,8 +2129,12 @@ if __name__ == "__main__":
|
||||
from helpers import send_mail
|
||||
|
||||
msg = stackprinter.format(exc)
|
||||
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
|
||||
"Exception from musicmuster", msg)
|
||||
send_mail(
|
||||
Config.ERRORS_TO,
|
||||
Config.ERRORS_FROM,
|
||||
"Exception from musicmuster",
|
||||
msg,
|
||||
)
|
||||
|
||||
print("\033[1;31;47mUnhandled exception starts")
|
||||
stackprinter.show(style="darkbg")
|
||||
|
||||
530
app/playlists.py
530
app/playlists.py
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,7 @@ import shutil
|
||||
import sys
|
||||
|
||||
from helpers import (
|
||||
fade_point,
|
||||
get_audio_segment,
|
||||
get_tags,
|
||||
leading_silence,
|
||||
trailing_silence,
|
||||
set_track_metadata,
|
||||
)
|
||||
|
||||
@ -29,7 +25,7 @@ process_tag_matches = True
|
||||
do_processing = True
|
||||
process_no_matches = True
|
||||
|
||||
source_dir = '/home/kae/music/Singles/tmp'
|
||||
source_dir = "/home/kae/music/Singles/tmp"
|
||||
parent_dir = os.path.dirname(source_dir)
|
||||
# #########################################################
|
||||
|
||||
@ -46,7 +42,7 @@ def main():
|
||||
# We only want to run this against the production database because
|
||||
# we will affect files in the common pool of tracks used by all
|
||||
# databases
|
||||
if 'musicmuster_prod' not in os.environ.get('MM_DB'):
|
||||
if "musicmuster_prod" not in os.environ.get("MM_DB"):
|
||||
response = input("Not on production database - c to continue: ")
|
||||
if response != "c":
|
||||
sys.exit(0)
|
||||
@ -68,8 +64,8 @@ def main():
|
||||
artists_to_path = {}
|
||||
for k, v in parents.items():
|
||||
try:
|
||||
titles_to_path[v['title'].lower()] = k
|
||||
artists_to_path[v['artist'].lower()] = k
|
||||
titles_to_path[v["title"].lower()] = k
|
||||
artists_to_path[v["artist"].lower()] = k
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
@ -78,33 +74,31 @@ def main():
|
||||
if not os.path.isfile(new_path):
|
||||
continue
|
||||
new_tags = get_tags(new_path)
|
||||
new_title = new_tags['title']
|
||||
new_artist = new_tags['artist']
|
||||
bitrate = new_tags['bitrate']
|
||||
new_title = new_tags["title"]
|
||||
new_artist = new_tags["artist"]
|
||||
bitrate = new_tags["bitrate"]
|
||||
|
||||
# If same filename exists in parent direcory, check tags
|
||||
parent_path = os.path.join(parent_dir, new_fname)
|
||||
if os.path.exists(parent_path):
|
||||
parent_tags = get_tags(parent_path)
|
||||
parent_title = parent_tags['title']
|
||||
parent_artist = parent_tags['artist']
|
||||
if (
|
||||
(str(parent_title).lower() == str(new_title).lower()) and
|
||||
(str(parent_artist).lower() == str(new_artist).lower())
|
||||
parent_title = parent_tags["title"]
|
||||
parent_artist = parent_tags["artist"]
|
||||
if (str(parent_title).lower() == str(new_title).lower()) and (
|
||||
str(parent_artist).lower() == str(new_artist).lower()
|
||||
):
|
||||
name_and_tags.append(
|
||||
f" {new_fname=}, {parent_title} → {new_title}, "
|
||||
f" {parent_artist} → {new_artist}"
|
||||
)
|
||||
if process_name_and_tags_matches:
|
||||
process_track(new_path, parent_path, new_title,
|
||||
new_artist, bitrate)
|
||||
process_track(new_path, parent_path, new_title, new_artist, bitrate)
|
||||
continue
|
||||
|
||||
# Check for matching tags although filename is different
|
||||
if new_title.lower() in titles_to_path:
|
||||
possible_path = titles_to_path[new_title.lower()]
|
||||
if parents[possible_path]['artist'].lower() == new_artist.lower():
|
||||
if parents[possible_path]["artist"].lower() == new_artist.lower():
|
||||
# print(
|
||||
# f"title={new_title}, artist={new_artist}:\n"
|
||||
# f" {new_path} → {parent_path}"
|
||||
@ -114,8 +108,9 @@ def main():
|
||||
f" {new_path} → {parent_path}"
|
||||
)
|
||||
if process_tag_matches:
|
||||
process_track(new_path, possible_path, new_title,
|
||||
new_artist, bitrate)
|
||||
process_track(
|
||||
new_path, possible_path, new_title, new_artist, bitrate
|
||||
)
|
||||
continue
|
||||
else:
|
||||
no_match += 1
|
||||
@ -132,8 +127,8 @@ def main():
|
||||
if choice:
|
||||
old_file = os.path.join(parent_dir, choice[0])
|
||||
oldtags = get_tags(old_file)
|
||||
old_title = oldtags['title']
|
||||
old_artist = oldtags['artist']
|
||||
old_title = oldtags["title"]
|
||||
old_artist = oldtags["artist"]
|
||||
print()
|
||||
print(f" File name will change {choice[0]}")
|
||||
print(f" → {new_fname}")
|
||||
@ -232,11 +227,8 @@ def main():
|
||||
|
||||
|
||||
def process_track(src, dst, title, artist, bitrate):
|
||||
|
||||
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
|
||||
print(
|
||||
f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n"
|
||||
)
|
||||
print(f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n")
|
||||
|
||||
if not do_processing:
|
||||
return
|
||||
|
||||
@ -4,13 +4,7 @@ import os
|
||||
|
||||
from config import Config
|
||||
from helpers import (
|
||||
fade_point,
|
||||
get_audio_segment,
|
||||
get_tags,
|
||||
leading_silence,
|
||||
normalise_track,
|
||||
set_track_metadata,
|
||||
trailing_silence,
|
||||
)
|
||||
from log import log
|
||||
from models import Tracks
|
||||
@ -72,12 +66,14 @@ def check_db(session):
|
||||
print("Invalid paths in database")
|
||||
print("-------------------------")
|
||||
for t in paths_not_found:
|
||||
print(f"""
|
||||
print(
|
||||
f"""
|
||||
Track ID: {t.id}
|
||||
Path: {t.path}
|
||||
Title: {t.title}
|
||||
Artist: {t.artist}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
if more_files_to_report:
|
||||
print("There were more paths than listed that were not found")
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
sys.path.append("app")
|
||||
import models
|
||||
|
||||
# Flake8 doesn't like the sys.append within imports
|
||||
# import sys
|
||||
# sys.path.append("app")
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def connection():
|
||||
engine = create_engine(
|
||||
@ -22,6 +22,7 @@ def connection():
|
||||
@pytest.fixture(scope="session")
|
||||
def setup_database(connection):
|
||||
from app.models import Base # noqa E402
|
||||
|
||||
Base.metadata.bind = connection
|
||||
Base.metadata.create_all()
|
||||
# seed_database()
|
||||
|
||||
1
devnotes.txt
Normal file
1
devnotes.txt
Normal file
@ -0,0 +1 @@
|
||||
Run Flake8 and Black
|
||||
BIN
docs/build/doctrees/environment.pickle
vendored
BIN
docs/build/doctrees/environment.pickle
vendored
Binary file not shown.
BIN
docs/build/doctrees/introduction.doctree
vendored
BIN
docs/build/doctrees/introduction.doctree
vendored
Binary file not shown.
24
docs/build/html/_sources/introduction.rst.txt
vendored
24
docs/build/html/_sources/introduction.rst.txt
vendored
@ -29,12 +29,10 @@ Features
|
||||
|
||||
* Database backed
|
||||
* Can be almost entirely keyboard driven
|
||||
* Playlist management
|
||||
* Easily add new tracks to playlists
|
||||
* Show multiple playlists on tabs
|
||||
* Open multiple playlists in tabs
|
||||
* Play tracks from any playlist
|
||||
* Add notes/comments to tracks on playlist
|
||||
* Automataic olour-coding of notes/comments according to content
|
||||
* Automatatic colour-coding of notes/comments according to content
|
||||
* Preview tracks before playing to audience
|
||||
* Time of day clock
|
||||
* Elapsed track time counter
|
||||
@ -42,8 +40,8 @@ Features
|
||||
* Time to run until track is silent
|
||||
* Graphic of volume from 5 seconds (configurable) before fade until
|
||||
track is silent
|
||||
* Ability to hide played tracks in playlist
|
||||
* Buttone to drop playout volume by 3dB for talkover
|
||||
* Optionally hide played tracks in playlist
|
||||
* Button to drop playout volume by 3dB for talkover
|
||||
* Playlist displays:
|
||||
* Title
|
||||
* Artist
|
||||
@ -51,18 +49,18 @@ Features
|
||||
* Estimated start time of track
|
||||
* Estimated end time of track
|
||||
* When track was last played
|
||||
* Bits per second (bps bitrate) of track
|
||||
* Length of silence in recording before music starts
|
||||
* Bits per second (bitrate) of track
|
||||
* Length of leading silence in recording before track starts
|
||||
* Total track length of arbitrary sections of tracks
|
||||
* Commands that are sent to OBS Studio (eg, for automated scene
|
||||
changes)
|
||||
* Playlist templates
|
||||
* Move selected/unplayed tracks between playlists
|
||||
* Down CSV of played tracks between arbitrary dates/times
|
||||
* Move selected or unplayed tracks between playlists
|
||||
* Download CSV of tracks played between arbitrary dates/times
|
||||
* Search for tracks by title or artist
|
||||
* Automatic search of current/next track in Wikipedia
|
||||
* Optional search of selected track in Wikipedia
|
||||
* Optional search of selected track in Songfacts
|
||||
* Automatic search of current and next track in Wikipedia
|
||||
* Optional search of any track in Wikipedia
|
||||
* Optional search of any track in Songfacts
|
||||
|
||||
|
||||
Requirements
|
||||
|
||||
24
docs/build/html/introduction.html
vendored
24
docs/build/html/introduction.html
vendored
@ -221,12 +221,10 @@ production of live internet radio shows.</p>
|
||||
<ul class="simple">
|
||||
<li><p>Database backed</p></li>
|
||||
<li><p>Can be almost entirely keyboard driven</p></li>
|
||||
<li><p>Playlist management</p></li>
|
||||
<li><p>Easily add new tracks to playlists</p></li>
|
||||
<li><p>Show multiple playlists on tabs</p></li>
|
||||
<li><p>Open multiple playlists in tabs</p></li>
|
||||
<li><p>Play tracks from any playlist</p></li>
|
||||
<li><p>Add notes/comments to tracks on playlist</p></li>
|
||||
<li><p>Automataic olour-coding of notes/comments according to content</p></li>
|
||||
<li><p>Automatatic colour-coding of notes/comments according to content</p></li>
|
||||
<li><p>Preview tracks before playing to audience</p></li>
|
||||
<li><p>Time of day clock</p></li>
|
||||
<li><p>Elapsed track time counter</p></li>
|
||||
@ -234,8 +232,8 @@ production of live internet radio shows.</p>
|
||||
<li><p>Time to run until track is silent</p></li>
|
||||
<li><p>Graphic of volume from 5 seconds (configurable) before fade until
|
||||
track is silent</p></li>
|
||||
<li><p>Ability to hide played tracks in playlist</p></li>
|
||||
<li><p>Buttone to drop playout volume by 3dB for talkover</p></li>
|
||||
<li><p>Optionally hide played tracks in playlist</p></li>
|
||||
<li><p>Button to drop playout volume by 3dB for talkover</p></li>
|
||||
<li><dl class="simple">
|
||||
<dt>Playlist displays:</dt><dd><ul>
|
||||
<li><p>Title</p></li>
|
||||
@ -244,8 +242,8 @@ track is silent</p></li>
|
||||
<li><p>Estimated start time of track</p></li>
|
||||
<li><p>Estimated end time of track</p></li>
|
||||
<li><p>When track was last played</p></li>
|
||||
<li><p>Bits per second (bps bitrate) of track</p></li>
|
||||
<li><p>Length of silence in recording before music starts</p></li>
|
||||
<li><p>Bits per second (bitrate) of track</p></li>
|
||||
<li><p>Length of leading silence in recording before track starts</p></li>
|
||||
<li><p>Total track length of arbitrary sections of tracks</p></li>
|
||||
<li><p>Commands that are sent to OBS Studio (eg, for automated scene
|
||||
changes)</p></li>
|
||||
@ -254,12 +252,12 @@ changes)</p></li>
|
||||
</dl>
|
||||
</li>
|
||||
<li><p>Playlist templates</p></li>
|
||||
<li><p>Move selected/unplayed tracks between playlists</p></li>
|
||||
<li><p>Down CSV of played tracks between arbitrary dates/times</p></li>
|
||||
<li><p>Move selected or unplayed tracks between playlists</p></li>
|
||||
<li><p>Download CSV of tracks played between arbitrary dates/times</p></li>
|
||||
<li><p>Search for tracks by title or artist</p></li>
|
||||
<li><p>Automatic search of current/next track in Wikipedia</p></li>
|
||||
<li><p>Optional search of selected track in Wikipedia</p></li>
|
||||
<li><p>Optional search of selected track in Songfacts</p></li>
|
||||
<li><p>Automatic search of current and next track in Wikipedia</p></li>
|
||||
<li><p>Optional search of any track in Wikipedia</p></li>
|
||||
<li><p>Optional search of any track in Songfacts</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="requirements">
|
||||
|
||||
2
docs/build/html/searchindex.js
vendored
2
docs/build/html/searchindex.js
vendored
@ -1 +1 @@
|
||||
Search.setIndex({"docnames": ["development", "index", "installation", "introduction", "reference", "tutorial"], "filenames": ["development.rst", "index.rst", "installation.rst", "introduction.rst", "reference.rst", "tutorial.rst"], "titles": ["Development", "Welcome to MusicMuster\u2019s documentation!", "Installation", "Introduction", "Reference", "Tutorial"], "terms": {"index": 1, "modul": 1, "search": [1, 3], "page": [1, 3], "i": 1, "music": [1, 3], "player": [1, 3], "target": 1, "product": [1, 3], "live": [1, 3], "internet": [1, 3], "radio": [1, 3], "show": [1, 3], "content": 3, "introduct": 1, "instal": [1, 3], "tutori": 1, "refer": 1, "develop": 1, "why": 1, "what": 1, "featur": 1, "requir": 1, "feedback": 1, "bug": 1, "etc": 1, "In": 3, "januari": 3, "2022": 3, "start": 3, "my": 3, "mixcloud": 3, "http": 3, "www": 3, "com": 3, "keithsmusicbox": 3, "until": 3, "had": 3, "been": 3, "an": 3, "station": 3, "which": 3, "me": 3, "us": 3, "window": 3, "playout": 3, "system": 3, "As": 3, "onli": 3, "linux": 3, "set": 3, "up": 3, "pc": 3, "specif": 3, "purpos": 3, "The": 3, "felt": 3, "were": 3, "shortcom": 3, "variou": 3, "area": 3, "onc": 3, "move": 3, "equival": 3, "didn": 3, "t": 3, "have": 3, "same": 3, "wa": 3, "unabl": 3, "find": 3, "one": 3, "met": 3, "criteria": 3, "decid": 3, "see": 3, "how": 3, "practic": 3, "would": 3, "write": 3, "own": 3, "born": 3, "It": 3, "base": 3, "whilst": 3, "could": 3, "gener": 3, "home": 3, "ar": 3, "much": 3, "better": 3, "applic": 3, "role": 3, "ha": 3, "design": 3, "support": 3, "databas": 3, "back": 3, "can": 3, "almost": 3, "entir": 3, "keyboard": 3, "driven": 3, "playlist": 3, "manag": 3, "easili": 3, "add": 3, "new": 3, "track": 3, "multipl": 3, "tab": 3, "plai": 3, "from": 3, "ani": 3, "note": 3, "comment": 3, "automata": 3, "olour": 3, "code": 3, "accord": 3, "preview": 3, "befor": 3, "audienc": 3, "time": 3, "dai": 3, "clock": 3, "elaps": 3, "counter": 3, "run": 3, "fade": 3, "silent": 3, "graphic": 3, "volum": 3, "5": 3, "second": 3, "configur": 3, "abil": 3, "hide": 3, "button": 3, "drop": 3, "3db": 3, "talkov": 3, "titl": 3, "artist": 3, "length": 3, "mm": 3, "ss": 3, "estim": 3, "end": 3, "when": 3, "last": 3, "bit": 3, "per": 3, "bp": 3, "bitrat": 3, "silenc": 3, "record": 3, "total": 3, "arbitrari": 3, "section": 3, "command": 3, "sent": 3, "ob": 3, "studio": 3, "eg": 3, "autom": 3, "scene": 3, "chang": 3, "templat": 3, "select": 3, "unplai": 3, "between": 3, "down": 3, "csv": 3, "date": 3, "automat": 3, "current": 3, "next": 3, "wikipedia": 3, "option": 3, "songfact": 3, "displai": 3, "test": 3, "debian": 3, "12": 3, "bookworm": 3, "howev": 3, "should": 3, "most": 3, "contemporari": 3, "explain": 3, "build": 3, "its": 3, "environ": 3, "automatc": 3, "all": 3, "except": 3, "version": 3, "mariadb": 3, "10": 3, "11": 3, "recent": 3, "suffic": 3, "python": 3, "3": 3, "8": 3, "later": 3, "pleas": 3, "send": 3, "keith": 3, "midnighthax": 3, "edmund": 3, "juli": 3, "2023": 3}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"welcom": 1, "musicmust": [1, 3], "": 1, "document": 1, "indic": 1, "tabl": 1, "content": 1, "develop": 0, "instal": 2, "introduct": 3, "refer": 4, "tutori": 5, "why": 3, "what": 3, "featur": 3, "requir": 3, "feedback": 3, "bug": 3, "etc": 3, "i": 3}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Welcome to MusicMuster\u2019s documentation!": [[1, "welcome-to-musicmuster-s-documentation"]], "Contents": [[1, "contents"]], "Indices and tables": [[1, "indices-and-tables"]], "Development": [[0, "development"]], "Installation": [[2, "installation"]], "Reference": [[4, "reference"]], "Tutorial": [[5, "tutorial"]], "Introduction": [[3, "introduction"]], "Why MusicMuster?": [[3, "why-musicmuster"]], "What is MusicMuster?": [[3, "what-is-musicmuster"]], "Features": [[3, "features"]], "Requirements": [[3, "requirements"]], "Feedback, bugs, etc": [[3, "feedback-bugs-etc"]]}, "indexentries": {}})
|
||||
Search.setIndex({"docnames": ["development", "index", "installation", "introduction", "reference", "tutorial"], "filenames": ["development.rst", "index.rst", "installation.rst", "introduction.rst", "reference.rst", "tutorial.rst"], "titles": ["Development", "Welcome to MusicMuster\u2019s documentation!", "Installation", "Introduction", "Reference", "Tutorial"], "terms": {"index": 1, "modul": 1, "search": [1, 3], "page": [1, 3], "i": 1, "music": [1, 3], "player": [1, 3], "target": 1, "product": [1, 3], "live": [1, 3], "internet": [1, 3], "radio": [1, 3], "show": [1, 3], "content": 3, "introduct": 1, "instal": [1, 3], "tutori": 1, "refer": 1, "develop": 1, "why": 1, "what": 1, "featur": 1, "requir": 1, "feedback": 1, "bug": 1, "etc": 1, "In": 3, "januari": 3, "2022": 3, "start": 3, "my": 3, "mixcloud": 3, "http": 3, "www": 3, "com": 3, "keithsmusicbox": 3, "until": 3, "had": 3, "been": 3, "an": 3, "station": 3, "which": 3, "me": 3, "us": 3, "window": 3, "playout": 3, "system": 3, "As": 3, "onli": 3, "linux": 3, "set": 3, "up": 3, "pc": 3, "specif": 3, "purpos": 3, "The": 3, "felt": 3, "were": 3, "shortcom": 3, "variou": 3, "area": 3, "onc": 3, "move": 3, "equival": 3, "didn": 3, "t": 3, "have": 3, "same": 3, "wa": 3, "unabl": 3, "find": 3, "one": 3, "met": 3, "criteria": 3, "decid": 3, "see": 3, "how": 3, "practic": 3, "would": 3, "write": 3, "own": 3, "born": 3, "It": 3, "base": 3, "whilst": 3, "could": 3, "gener": 3, "home": 3, "ar": 3, "much": 3, "better": 3, "applic": 3, "role": 3, "ha": 3, "design": 3, "support": 3, "databas": 3, "back": 3, "can": 3, "almost": 3, "entir": 3, "keyboard": 3, "driven": 3, "playlist": 3, "manag": [], "easili": [], "add": 3, "new": [], "track": 3, "multipl": 3, "tab": 3, "plai": 3, "from": 3, "ani": 3, "note": 3, "comment": 3, "automata": [], "olour": [], "code": 3, "accord": 3, "preview": 3, "befor": 3, "audienc": 3, "time": 3, "dai": 3, "clock": 3, "elaps": 3, "counter": 3, "run": 3, "fade": 3, "silent": 3, "graphic": 3, "volum": 3, "5": 3, "second": 3, "configur": 3, "abil": [], "hide": 3, "button": 3, "drop": 3, "3db": 3, "talkov": 3, "titl": 3, "artist": 3, "length": 3, "mm": 3, "ss": 3, "estim": 3, "end": 3, "when": 3, "last": 3, "bit": 3, "per": 3, "bp": [], "bitrat": 3, "silenc": 3, "record": 3, "total": 3, "arbitrari": 3, "section": 3, "command": 3, "sent": 3, "ob": 3, "studio": 3, "eg": 3, "autom": 3, "scene": 3, "chang": 3, "templat": 3, "select": 3, "unplai": 3, "between": 3, "down": [], "csv": 3, "date": 3, "automat": 3, "current": 3, "next": 3, "wikipedia": 3, "option": 3, "songfact": 3, "displai": 3, "test": 3, "debian": 3, "12": 3, "bookworm": 3, "howev": 3, "should": 3, "most": 3, "contemporari": 3, "explain": 3, "build": 3, "its": 3, "environ": 3, "automatc": 3, "all": 3, "except": 3, "version": 3, "mariadb": 3, "10": 3, "11": 3, "recent": 3, "suffic": 3, "python": 3, "3": 3, "8": 3, "later": 3, "pleas": 3, "send": 3, "keith": 3, "midnighthax": 3, "edmund": 3, "juli": 3, "2023": 3, "open": 3, "automatat": 3, "colour": 3, "lead": 3, "download": 3}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"welcom": 1, "musicmust": [1, 3], "": 1, "document": 1, "indic": 1, "tabl": 1, "content": 1, "develop": 0, "instal": 2, "introduct": 3, "refer": 4, "tutori": 5, "why": 3, "what": 3, "featur": 3, "requir": 3, "feedback": 3, "bug": 3, "etc": 3, "i": 3}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Welcome to MusicMuster\u2019s documentation!": [[1, "welcome-to-musicmuster-s-documentation"]], "Contents": [[1, "contents"]], "Indices and tables": [[1, "indices-and-tables"]], "Development": [[0, "development"]], "Installation": [[2, "installation"]], "Reference": [[4, "reference"]], "Tutorial": [[5, "tutorial"]], "Introduction": [[3, "introduction"]], "Why MusicMuster?": [[3, "why-musicmuster"]], "What is MusicMuster?": [[3, "what-is-musicmuster"]], "Features": [[3, "features"]], "Requirements": [[3, "requirements"]], "Feedback, bugs, etc": [[3, "feedback-bugs-etc"]]}, "indexentries": {}})
|
||||
@ -29,11 +29,10 @@ Features
|
||||
|
||||
* Database backed
|
||||
* Can be almost entirely keyboard driven
|
||||
* Easily add new tracks to playlists
|
||||
* Show multiple playlists on tabs
|
||||
* Open multiple playlists in tabs
|
||||
* Play tracks from any playlist
|
||||
* Add notes/comments to tracks on playlist
|
||||
* Automataic olour-coding of notes/comments according to content
|
||||
* Automatatic colour-coding of notes/comments according to content
|
||||
* Preview tracks before playing to audience
|
||||
* Time of day clock
|
||||
* Elapsed track time counter
|
||||
@ -41,8 +40,8 @@ Features
|
||||
* Time to run until track is silent
|
||||
* Graphic of volume from 5 seconds (configurable) before fade until
|
||||
track is silent
|
||||
* Ability to hide played tracks in playlist
|
||||
* Buttone to drop playout volume by 3dB for talkover
|
||||
* Optionally hide played tracks in playlist
|
||||
* Button to drop playout volume by 3dB for talkover
|
||||
* Playlist displays:
|
||||
* Title
|
||||
* Artist
|
||||
@ -50,18 +49,18 @@ Features
|
||||
* Estimated start time of track
|
||||
* Estimated end time of track
|
||||
* When track was last played
|
||||
* Bits per second (bps bitrate) of track
|
||||
* Length of silence in recording before music starts
|
||||
* Bits per second (bitrate) of track
|
||||
* Length of leading silence in recording before track starts
|
||||
* Total track length of arbitrary sections of tracks
|
||||
* Commands that are sent to OBS Studio (eg, for automated scene
|
||||
changes)
|
||||
* Playlist templates
|
||||
* Move selected/unplayed tracks between playlists
|
||||
* Down CSV of played tracks between arbitrary dates/times
|
||||
* Move selected or unplayed tracks between playlists
|
||||
* Download CSV of tracks played between arbitrary dates/times
|
||||
* Search for tracks by title or artist
|
||||
* Automatic search of current/next track in Wikipedia
|
||||
* Optional search of selected track in Wikipedia
|
||||
* Optional search of selected track in Songfacts
|
||||
* Automatic search of current and next track in Wikipedia
|
||||
* Optional search of any track in Wikipedia
|
||||
* Optional search of any track in Songfacts
|
||||
|
||||
|
||||
Requirements
|
||||
|
||||
802
poetry.lock
generated
802
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -42,6 +42,8 @@ mypy = "^0.991"
|
||||
pudb = "^2022.1.3"
|
||||
sphinx = "^7.0.1"
|
||||
furo = "^2023.5.20"
|
||||
black = "^23.3.0"
|
||||
flakehell = "^0.9.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
7
test.py
7
test.py
@ -2,6 +2,7 @@
|
||||
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
|
||||
class TabBar(QtWidgets.QTabBar):
|
||||
def paintEvent(self, event):
|
||||
painter = QtWidgets.QStylePainter(self)
|
||||
@ -13,16 +14,18 @@ class TabBar(QtWidgets.QTabBar):
|
||||
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
|
||||
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
|
||||
|
||||
|
||||
class Window(QtWidgets.QTabWidget):
|
||||
def __init__(self):
|
||||
QtWidgets.QTabWidget.__init__(self)
|
||||
self.setTabBar(TabBar(self))
|
||||
for color in 'tomato orange yellow lightgreen skyblue plum'.split():
|
||||
for color in "tomato orange yellow lightgreen skyblue plum".split():
|
||||
self.addTab(QtWidgets.QWidget(self), color)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
window = Window()
|
||||
window.resize(420, 200)
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
from config import Config
|
||||
from datetime import datetime, timedelta
|
||||
from helpers import *
|
||||
from models import Tracks
|
||||
from helpers import (
|
||||
fade_point,
|
||||
get_audio_segment,
|
||||
get_tags,
|
||||
get_relative_date,
|
||||
leading_silence,
|
||||
ms_to_mmss,
|
||||
)
|
||||
|
||||
|
||||
def test_fade_point():
|
||||
@ -18,8 +23,8 @@ def test_fade_point():
|
||||
testdata = eval(f.read())
|
||||
|
||||
# Volume detection can vary, so ± 1 second is OK
|
||||
assert fade_at < testdata['fade_at'] + 1000
|
||||
assert fade_at > testdata['fade_at'] - 1000
|
||||
assert fade_at < testdata["fade_at"] + 1000
|
||||
assert fade_at > testdata["fade_at"] - 1000
|
||||
|
||||
|
||||
def test_get_tags():
|
||||
@ -32,8 +37,8 @@ def test_get_tags():
|
||||
with open(test_track_data) as f:
|
||||
testdata = eval(f.read())
|
||||
|
||||
assert tags['artist'] == testdata['artist']
|
||||
assert tags['title'] == testdata['title']
|
||||
assert tags["artist"] == testdata["artist"]
|
||||
assert tags["title"] == testdata["title"]
|
||||
|
||||
|
||||
def test_get_relative_date():
|
||||
@ -44,8 +49,7 @@ def test_get_relative_date():
|
||||
eight_days_ago = today_at_10 - timedelta(days=8)
|
||||
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
||||
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
||||
assert get_relative_date(
|
||||
sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
||||
assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
||||
|
||||
|
||||
def test_leading_silence():
|
||||
@ -62,8 +66,8 @@ def test_leading_silence():
|
||||
testdata = eval(f.read())
|
||||
|
||||
# Volume detection can vary, so ± 1 second is OK
|
||||
assert silence_at < testdata['leading_silence'] + 1000
|
||||
assert silence_at > testdata['leading_silence'] - 1000
|
||||
assert silence_at < testdata["leading_silence"] + 1000
|
||||
assert silence_at > testdata["leading_silence"] - 1000
|
||||
|
||||
|
||||
def test_ms_to_mmss():
|
||||
|
||||
@ -135,7 +135,6 @@ def test_playdates_remove_track(session):
|
||||
track_path = "/a/b/c"
|
||||
track = Tracks(session, track_path)
|
||||
|
||||
playdate = Playdates(session, track.id)
|
||||
Playdates.remove_track(session, track.id)
|
||||
|
||||
last_played = Playdates.last_played(session, track.id)
|
||||
@ -149,10 +148,8 @@ def test_playlist_create(session):
|
||||
|
||||
def test_playlist_add_note(session):
|
||||
note_text = "my note"
|
||||
note_row = 2
|
||||
|
||||
playlist = Playlists(session, "my playlist")
|
||||
note = playlist.add_note(session, note_row, note_text)
|
||||
|
||||
assert len(playlist.notes) == 1
|
||||
playlist_note = playlist.notes[0]
|
||||
@ -356,9 +353,7 @@ def test_tracks_get_all_paths(session):
|
||||
def test_tracks_get_all_tracks(session):
|
||||
# Need two tracks
|
||||
track1_path = "/a/b/c"
|
||||
track1 = Tracks(session, track1_path)
|
||||
track2_path = "/m/n/o"
|
||||
track2 = Tracks(session, track2_path)
|
||||
|
||||
result = Tracks.get_all_tracks(session)
|
||||
assert track1_path in [a.path for a in result]
|
||||
@ -369,9 +364,7 @@ def test_tracks_by_filename(session):
|
||||
track1_path = "/a/b/c"
|
||||
|
||||
track1 = Tracks(session, track1_path)
|
||||
assert Tracks.get_by_filename(
|
||||
session, os.path.basename(track1_path)
|
||||
) is track1
|
||||
assert Tracks.get_by_filename(session, os.path.basename(track1_path)) is track1
|
||||
|
||||
|
||||
def test_tracks_by_path(session):
|
||||
@ -403,26 +396,24 @@ def test_tracks_rescan(session):
|
||||
# Re-read the track
|
||||
track_read = Tracks.get_by_path(session, test_track_path)
|
||||
|
||||
assert track_read.duration == testdata['duration']
|
||||
assert track_read.start_gap == testdata['leading_silence']
|
||||
assert track_read.duration == testdata["duration"]
|
||||
assert track_read.start_gap == testdata["leading_silence"]
|
||||
# Silence detection can vary, so ± 1 second is OK
|
||||
assert track_read.fade_at < testdata['fade_at'] + 1000
|
||||
assert track_read.fade_at > testdata['fade_at'] - 1000
|
||||
assert track_read.silence_at < testdata['trailing_silence'] + 1000
|
||||
assert track_read.silence_at > testdata['trailing_silence'] - 1000
|
||||
assert track_read.fade_at < testdata["fade_at"] + 1000
|
||||
assert track_read.fade_at > testdata["fade_at"] - 1000
|
||||
assert track_read.silence_at < testdata["trailing_silence"] + 1000
|
||||
assert track_read.silence_at > testdata["trailing_silence"] - 1000
|
||||
|
||||
|
||||
def test_tracks_remove_by_path(session):
|
||||
track1_path = "/a/b/c"
|
||||
|
||||
track1 = Tracks(session, track1_path)
|
||||
assert len(Tracks.get_all_tracks(session)) == 1
|
||||
Tracks.remove_by_path(session, track1_path)
|
||||
assert len(Tracks.get_all_tracks(session)) == 0
|
||||
|
||||
|
||||
def test_tracks_search_artists(session):
|
||||
|
||||
track1_path = "/a/b/c"
|
||||
track1_artist = "Artist One"
|
||||
track1 = Tracks(session, track1_path)
|
||||
@ -435,7 +426,6 @@ def test_tracks_search_artists(session):
|
||||
|
||||
session.commit()
|
||||
|
||||
x = Tracks.get_all_tracks(session)
|
||||
artist_first_word = track1_artist.split()[0].lower()
|
||||
assert len(Tracks.search_artists(session, artist_first_word)) == 2
|
||||
assert len(Tracks.search_artists(session, track1_artist)) == 1
|
||||
@ -454,7 +444,6 @@ def test_tracks_search_titles(session):
|
||||
|
||||
session.commit()
|
||||
|
||||
x = Tracks.get_all_tracks(session)
|
||||
title_first_word = track1_title.split()[0].lower()
|
||||
assert len(Tracks.search_titles(session, title_first_word)) == 2
|
||||
assert len(Tracks.search_titles(session, track1_title)) == 1
|
||||
|
||||
@ -3,7 +3,6 @@ from PyQt5.QtCore import Qt
|
||||
from app import playlists
|
||||
from app import models
|
||||
from app import musicmuster
|
||||
from app import dbconfig
|
||||
|
||||
|
||||
def seed2tracks(session):
|
||||
@ -78,7 +77,6 @@ def test_save_and_restore(qtbot, session):
|
||||
|
||||
|
||||
def test_meta_all_clear(qtbot, session):
|
||||
|
||||
# Create playlist
|
||||
playlist = models.Playlists(session, "my playlist")
|
||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||
@ -107,7 +105,6 @@ def test_meta_all_clear(qtbot, session):
|
||||
|
||||
|
||||
def test_meta(qtbot, session):
|
||||
|
||||
# Create playlist
|
||||
playlist = playlists.Playlists(session, "my playlist")
|
||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||
@ -211,7 +208,6 @@ def test_clear_next(qtbot, session):
|
||||
|
||||
|
||||
def test_get_selected_row(qtbot, monkeypatch, session):
|
||||
|
||||
monkeypatch.setattr(musicmuster, "Session", session)
|
||||
monkeypatch.setattr(playlists, "Session", session)
|
||||
|
||||
@ -236,15 +232,12 @@ def test_get_selected_row(qtbot, monkeypatch, session):
|
||||
row0_item0 = playlist_tab.item(0, 0)
|
||||
assert row0_item0 is not None
|
||||
rect = playlist_tab.visualItemRect(row0_item0)
|
||||
qtbot.mouseClick(
|
||||
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
|
||||
)
|
||||
qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
|
||||
row_number = playlist_tab.get_selected_row()
|
||||
assert row_number == 0
|
||||
|
||||
|
||||
def test_set_next(qtbot, monkeypatch, session):
|
||||
|
||||
monkeypatch.setattr(musicmuster, "Session", session)
|
||||
monkeypatch.setattr(playlists, "Session", session)
|
||||
seed2tracks(session)
|
||||
@ -274,14 +267,11 @@ def test_set_next(qtbot, monkeypatch, session):
|
||||
row0_item2 = playlist_tab.item(0, 2)
|
||||
assert row0_item2 is not None
|
||||
rect = playlist_tab.visualItemRect(row0_item2)
|
||||
qtbot.mouseClick(
|
||||
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
|
||||
)
|
||||
qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
|
||||
selected_title = playlist_tab.get_selected_title()
|
||||
assert selected_title == track1_title
|
||||
|
||||
qtbot.keyPress(playlist_tab.viewport(), "N",
|
||||
modifier=Qt.ControlModifier)
|
||||
qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier)
|
||||
qtbot.wait(1000)
|
||||
|
||||
|
||||
|
||||
24
tree.py
24
tree.py
@ -9,9 +9,10 @@ datas = {
|
||||
],
|
||||
"No Category": [
|
||||
("New Game", "Playnite", "", "", "Never", "Not Plated", ""),
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class GroupDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def __init__(self, parent=None):
|
||||
super(GroupDelegate, self).__init__(parent)
|
||||
@ -25,6 +26,7 @@ class GroupDelegate(QtWidgets.QStyledItemDelegate):
|
||||
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
|
||||
option.icon = self._minus_icon if is_open else self._plus_icon
|
||||
|
||||
|
||||
class GroupView(QtWidgets.QTreeView):
|
||||
def __init__(self, model, parent=None):
|
||||
super(GroupView, self).__init__(parent)
|
||||
@ -48,7 +50,18 @@ class GroupModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, parent=None):
|
||||
super(GroupModel, self).__init__(parent)
|
||||
self.setColumnCount(8)
|
||||
self.setHorizontalHeaderLabels(["", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""])
|
||||
self.setHorizontalHeaderLabels(
|
||||
[
|
||||
"",
|
||||
"Name",
|
||||
"Library",
|
||||
"Release Date",
|
||||
"Genre(s)",
|
||||
"Last Played",
|
||||
"Time Played",
|
||||
"",
|
||||
]
|
||||
)
|
||||
for i in range(self.columnCount()):
|
||||
it = self.horizontalHeaderItem(i)
|
||||
it.setForeground(QtGui.QColor("#F2F2F2"))
|
||||
@ -84,7 +97,7 @@ class GroupModel(QtGui.QStandardItemModel):
|
||||
item.setEditable(False)
|
||||
item.setBackground(QtGui.QColor("#0D1225"))
|
||||
item.setForeground(QtGui.QColor("#F2F2F2"))
|
||||
group_item.setChild(j, i+1, item)
|
||||
group_item.setChild(j, i + 1, item)
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
@ -100,11 +113,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
for children in childrens:
|
||||
model.append_element_to_group(group_item, children)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = MainWindow()
|
||||
w.resize(720, 240)
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user