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))}]")
|
||||
|
||||
125
app/helpers.py
125
app/helpers.py
@ -1,4 +1,3 @@
|
||||
import numpy as np
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
@ -10,13 +9,13 @@ from config import Config
|
||||
from datetime import datetime
|
||||
from email.message import EmailMessage
|
||||
from log import log
|
||||
from mutagen.flac import FLAC # type: ignore
|
||||
from mutagen.mp3 import MP3 # type: ignore
|
||||
from mutagen.flac import FLAC # type: ignore
|
||||
from mutagen.mp3 import MP3 # type: ignore
|
||||
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 PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
||||
from tinytag import TinyTag # type: ignore
|
||||
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,10 +80,10 @@ 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'):
|
||||
return AudioSegment.from_file(path, "flac") # type: ignore
|
||||
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.
|
||||
|
||||
@ -145,9 +149,10 @@ 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:
|
||||
audio_segment: AudioSegment,
|
||||
silence_threshold: int = Config.DBFS_SILENCE,
|
||||
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)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import urllib.parse
|
||||
|
||||
from datetime import datetime
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Dict, Optional
|
||||
from PyQt6.QtCore import QUrl # type: ignore
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Dict
|
||||
from PyQt6.QtCore import QUrl # type: ignore
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
from config import Config
|
||||
@ -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)
|
||||
|
||||
|
||||
318
app/models.py
318
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,
|
||||
name: Optional[str] = None,
|
||||
duration: Optional[int] = None, path: Optional[str] = None,
|
||||
enabled: bool = True) -> None:
|
||||
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:
|
||||
"""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,13 +372,14 @@ class PlaylistRows(Base):
|
||||
f"note={self.note}, plr_rownum={self.plr_rownum}>"
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
session: scoped_session,
|
||||
playlist_id: int,
|
||||
track_id: Optional[int],
|
||||
row_number: int,
|
||||
note: str = ""
|
||||
) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
playlist_id: int,
|
||||
track_id: Optional[int],
|
||||
row_number: int,
|
||||
note: str = "",
|
||||
) -> None:
|
||||
"""Create PlaylistRows object"""
|
||||
|
||||
self.playlist_id = playlist_id
|
||||
@ -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(
|
||||
select(PlaylistRows)
|
||||
.where(PlaylistRows.playlist_id == playlist_id)
|
||||
.order_by(PlaylistRows.plr_rownum)
|
||||
).scalars().all()
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(PlaylistRows)
|
||||
.where(PlaylistRows.playlist_id == playlist_id)
|
||||
.order_by(PlaylistRows.plr_rownum)
|
||||
)
|
||||
.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(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.id.in_(plr_ids)
|
||||
).order_by(cls.plr_rownum)).scalars().all()
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.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(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.played.is_(True)
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(cls.playlist_id == playlist_id, cls.played.is_(True))
|
||||
.order_by(cls.plr_rownum)
|
||||
)
|
||||
.order_by(cls.plr_rownum)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return plrs
|
||||
|
||||
@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(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.track_id.is_not(None),
|
||||
cls.played.is_(False)
|
||||
plrs = (
|
||||
session.execute(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.playlist_id == playlist_id,
|
||||
cls.track_id.is_not(None),
|
||||
cls.played.is_(False),
|
||||
)
|
||||
.order_by(cls.plr_rownum)
|
||||
)
|
||||
.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")
|
||||
|
||||
@ -653,18 +649,18 @@ class Tracks(Base):
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
path: str,
|
||||
title: Optional[str] = None,
|
||||
artist: Optional[str] = None,
|
||||
duration: int = 0,
|
||||
start_gap: int = 0,
|
||||
fade_at: Optional[int] = None,
|
||||
silence_at: Optional[int] = None,
|
||||
mtime: Optional[float] = None,
|
||||
lastplayed: Optional[datetime] = None,
|
||||
) -> None:
|
||||
self,
|
||||
session: scoped_session,
|
||||
path: str,
|
||||
title: Optional[str] = None,
|
||||
artist: Optional[str] = None,
|
||||
duration: int = 0,
|
||||
start_gap: int = 0,
|
||||
fade_at: Optional[int] = None,
|
||||
silence_at: Optional[int] = None,
|
||||
mtime: Optional[float] = None,
|
||||
lastplayed: Optional[datetime] = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
@ -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)
|
||||
).scalar_one()
|
||||
)
|
||||
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()
|
||||
|
||||
13
app/music.py
13
app/music.py
@ -1,16 +1,16 @@
|
||||
# import os
|
||||
import threading
|
||||
import vlc # type: ignore
|
||||
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
|
||||
|
||||
from log import log
|
||||
|
||||
from PyQt6.QtCore import ( # type: ignore
|
||||
from PyQt6.QtCore import ( # type: ignore
|
||||
QRunnable,
|
||||
QThreadPool,
|
||||
)
|
||||
@ -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.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
572
app/playlists.py
572
app/playlists.py
File diff suppressed because it is too large
Load Diff
@ -5,16 +5,12 @@
|
||||
# parent (eg, bettet bitrate).
|
||||
|
||||
import os
|
||||
import pydymenu # type: ignore
|
||||
import pydymenu # type: ignore
|
||||
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,44 +74,43 @@ 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}"
|
||||
# )
|
||||
tags_not_name.append(
|
||||
f"title={new_title}, artist={new_artist}:\n"
|
||||
f" {new_path} → {parent_path}"
|
||||
)
|
||||
f"title={new_title}, artist={new_artist}:\n"
|
||||
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")
|
||||
|
||||
|
||||
11
conftest.py
11
conftest.py
@ -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(
|
||||
@ -21,7 +21,8 @@ def connection():
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def setup_database(connection):
|
||||
from app.models import Base # noqa E402
|
||||
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)
|
||||
@ -141,7 +138,7 @@ def test_meta(qtbot, session):
|
||||
|
||||
# Add a note
|
||||
note_text = "my note"
|
||||
note_row = 7 # will be added as row 3
|
||||
note_row = 7 # will be added as row 3
|
||||
note = models.Notes(session, playlist.id, note_row, note_text)
|
||||
playlist_tab._insert_note(session, note)
|
||||
|
||||
@ -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