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.QtCore import Qt, QEvent, QObject
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QAbstractItemDelegate,
|
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QApplication,
|
QApplication,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QStyledItemDelegate,
|
QStyledItemDelegate,
|
||||||
QStyleOptionViewItem,
|
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
)
|
)
|
||||||
@ -33,16 +31,15 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
if event.type() == QEvent.Type.KeyPress:
|
if event.type() == QEvent.Type.KeyPress:
|
||||||
key_event = cast(QKeyEvent, event)
|
key_event = cast(QKeyEvent, event)
|
||||||
if key_event.key() == Qt.Key.Key_Return:
|
if key_event.key() == Qt.Key.Key_Return:
|
||||||
if key_event.modifiers() == (
|
if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier):
|
||||||
Qt.KeyboardModifier.ControlModifier
|
|
||||||
):
|
|
||||||
print("save data")
|
print("save data")
|
||||||
self.commitData.emit(editor)
|
self.commitData.emit(editor)
|
||||||
self.closeEditor.emit(editor)
|
self.closeEditor.emit(editor)
|
||||||
return True
|
return True
|
||||||
elif key_event.key() == Qt.Key.Key_Escape:
|
elif key_event.key() == Qt.Key.Key_Escape:
|
||||||
discard_edits = QMessageBox.question(
|
discard_edits = QMessageBox.question(
|
||||||
self.parent(), "Abandon edit", "Discard changes?")
|
self.parent(), "Abandon edit", "Discard changes?"
|
||||||
|
)
|
||||||
if discard_edits == QMessageBox.StandardButton.Yes:
|
if discard_edits == QMessageBox.StandardButton.Yes:
|
||||||
print("abandon edit")
|
print("abandon edit")
|
||||||
self.closeEditor.emit(editor)
|
self.closeEditor.emit(editor)
|
||||||
@ -74,7 +71,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.table_widget.resizeRowsToContents()
|
self.table_widget.resizeRowsToContents()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pydub import AudioSegment, effects
|
from pydub import AudioSegment
|
||||||
|
|
||||||
# DIR = "/home/kae/git/musicmuster/archive"
|
# DIR = "/home/kae/git/musicmuster/archive"
|
||||||
DIR = "/home/kae/git/musicmuster"
|
DIR = "/home/kae/git/musicmuster"
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
from config import Config
|
from config import Config
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import (sessionmaker, scoped_session)
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from log import log
|
from log import log
|
||||||
|
|
||||||
MYSQL_CONNECT = os.environ.get('MM_DB')
|
MYSQL_CONNECT = os.environ.get("MM_DB")
|
||||||
if MYSQL_CONNECT is None:
|
if MYSQL_CONNECT is None:
|
||||||
raise ValueError("MYSQL_CONNECT is undefined")
|
raise ValueError("MYSQL_CONNECT is undefined")
|
||||||
else:
|
else:
|
||||||
dbname = MYSQL_CONNECT.split('/')[-1]
|
dbname = MYSQL_CONNECT.split("/")[-1]
|
||||||
log.debug(f"Database: {dbname}")
|
log.debug(f"Database: {dbname}")
|
||||||
|
|
||||||
# MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION')
|
# MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION')
|
||||||
@ -31,10 +30,10 @@ else:
|
|||||||
|
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
MYSQL_CONNECT,
|
MYSQL_CONNECT,
|
||||||
encoding='utf-8',
|
encoding="utf-8",
|
||||||
echo=Config.DISPLAY_SQL,
|
echo=Config.DISPLAY_SQL,
|
||||||
pool_pre_ping=True,
|
pool_pre_ping=True,
|
||||||
future=True
|
future=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -47,8 +46,7 @@ def Session() -> Generator[scoped_session, None, None]:
|
|||||||
Session = scoped_session(sessionmaker(bind=engine, future=True))
|
Session = scoped_session(sessionmaker(bind=engine, future=True))
|
||||||
log.debug(f"SqlA: session acquired [{hex(id(Session))}]")
|
log.debug(f"SqlA: session acquired [{hex(id(Session))}]")
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Session acquisition: {file}:{function}:{lineno} "
|
f"Session acquisition: {file}:{function}:{lineno} " f"[{hex(id(Session))}]"
|
||||||
f"[{hex(id(Session))}]"
|
|
||||||
)
|
)
|
||||||
yield Session
|
yield Session
|
||||||
log.debug(f" SqlA: session released [{hex(id(Session))}]")
|
log.debug(f" SqlA: session released [{hex(id(Session))}]")
|
||||||
|
|||||||
125
app/helpers.py
125
app/helpers.py
@ -1,4 +1,3 @@
|
|||||||
import numpy as np
|
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
import shutil
|
import shutil
|
||||||
@ -10,13 +9,13 @@ from config import Config
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from log import log
|
from log import log
|
||||||
from mutagen.flac import FLAC # type: ignore
|
from mutagen.flac import FLAC # type: ignore
|
||||||
from mutagen.mp3 import MP3 # type: ignore
|
from mutagen.mp3 import MP3 # type: ignore
|
||||||
from pydub import AudioSegment, effects
|
from pydub import AudioSegment, effects
|
||||||
from pydub.utils import mediainfo
|
from pydub.utils import mediainfo
|
||||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
||||||
from tinytag import TinyTag # type: ignore
|
from tinytag import TinyTag # type: ignore
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
||||||
@ -26,7 +25,8 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
|||||||
dlg.setWindowTitle(title)
|
dlg.setWindowTitle(title)
|
||||||
dlg.setText(question)
|
dlg.setText(question)
|
||||||
dlg.setStandardButtons(
|
dlg.setStandardButtons(
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
dlg.setIcon(QMessageBox.Icon.Question)
|
dlg.setIcon(QMessageBox.Icon.Question)
|
||||||
if default_yes:
|
if default_yes:
|
||||||
dlg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
dlg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
||||||
@ -36,8 +36,10 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def fade_point(
|
def fade_point(
|
||||||
audio_segment: AudioSegment, fade_threshold: float = 0.0,
|
audio_segment: AudioSegment,
|
||||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
|
fade_threshold: float = 0.0,
|
||||||
|
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the millisecond/index of the point where the volume drops below
|
Returns the millisecond/index of the point where the volume drops below
|
||||||
the maximum and doesn't get louder again.
|
the maximum and doesn't get louder again.
|
||||||
@ -55,8 +57,9 @@ def fade_point(
|
|||||||
fade_threshold = max_vol
|
fade_threshold = max_vol
|
||||||
|
|
||||||
while (
|
while (
|
||||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
|
||||||
and trim_ms > 0): # noqa W503
|
and trim_ms > 0
|
||||||
|
): # noqa W503
|
||||||
trim_ms -= chunk_size
|
trim_ms -= chunk_size
|
||||||
|
|
||||||
# if there is no trailing silence, return lenght of track (it's less
|
# if there is no trailing silence, return lenght of track (it's less
|
||||||
@ -77,10 +80,10 @@ def file_is_unreadable(path: Optional[str]) -> bool:
|
|||||||
|
|
||||||
def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
||||||
try:
|
try:
|
||||||
if path.endswith('.mp3'):
|
if path.endswith(".mp3"):
|
||||||
return AudioSegment.from_mp3(path)
|
return AudioSegment.from_mp3(path)
|
||||||
elif path.endswith('.flac'):
|
elif path.endswith(".flac"):
|
||||||
return AudioSegment.from_file(path, "flac") # type: ignore
|
return AudioSegment.from_file(path, "flac") # type: ignore
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -99,12 +102,13 @@ def get_tags(path: str) -> Dict[str, Any]:
|
|||||||
artist=tag.artist,
|
artist=tag.artist,
|
||||||
bitrate=round(tag.bitrate),
|
bitrate=round(tag.bitrate),
|
||||||
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
|
||||||
path=path
|
path=path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_relative_date(past_date: Optional[datetime],
|
def get_relative_date(
|
||||||
reference_date: Optional[datetime] = None) -> str:
|
past_date: Optional[datetime], reference_date: Optional[datetime] = None
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Return how long before reference_date past_date is as string.
|
Return how long before reference_date past_date is as string.
|
||||||
|
|
||||||
@ -145,9 +149,10 @@ def get_relative_date(past_date: Optional[datetime],
|
|||||||
|
|
||||||
|
|
||||||
def leading_silence(
|
def leading_silence(
|
||||||
audio_segment: AudioSegment,
|
audio_segment: AudioSegment,
|
||||||
silence_threshold: int = Config.DBFS_SILENCE,
|
silence_threshold: int = Config.DBFS_SILENCE,
|
||||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
|
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the millisecond/index that the leading silence ends.
|
Returns the millisecond/index that the leading silence ends.
|
||||||
audio_segment - the segment to find silence in
|
audio_segment - the segment to find silence in
|
||||||
@ -159,9 +164,11 @@ def leading_silence(
|
|||||||
|
|
||||||
trim_ms: int = 0 # ms
|
trim_ms: int = 0 # ms
|
||||||
assert chunk_size > 0 # to avoid infinite loop
|
assert chunk_size > 0 # to avoid infinite loop
|
||||||
while (
|
while audio_segment[
|
||||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
|
trim_ms : trim_ms + chunk_size
|
||||||
silence_threshold and trim_ms < len(audio_segment)):
|
].dBFS < silence_threshold and trim_ms < len( # noqa W504
|
||||||
|
audio_segment
|
||||||
|
):
|
||||||
trim_ms += chunk_size
|
trim_ms += chunk_size
|
||||||
|
|
||||||
# if there is no end it should return the length of the segment
|
# if there is no end it should return the length of the segment
|
||||||
@ -175,9 +182,9 @@ def send_mail(to_addr, from_addr, subj, body):
|
|||||||
msg = EmailMessage()
|
msg = EmailMessage()
|
||||||
msg.set_content(body)
|
msg.set_content(body)
|
||||||
|
|
||||||
msg['Subject'] = subj
|
msg["Subject"] = subj
|
||||||
msg['From'] = from_addr
|
msg["From"] = from_addr
|
||||||
msg['To'] = to_addr
|
msg["To"] = to_addr
|
||||||
|
|
||||||
# Send the message via SMTP server.
|
# Send the message via SMTP server.
|
||||||
context = ssl.create_default_context()
|
context = ssl.create_default_context()
|
||||||
@ -194,8 +201,7 @@ def send_mail(to_addr, from_addr, subj, body):
|
|||||||
s.quit()
|
s.quit()
|
||||||
|
|
||||||
|
|
||||||
def ms_to_mmss(ms: Optional[int], decimals: int = 0,
|
def ms_to_mmss(ms: Optional[int], decimals: int = 0, negative: bool = False) -> str:
|
||||||
negative: bool = False) -> str:
|
|
||||||
"""Convert milliseconds to mm:ss"""
|
"""Convert milliseconds to mm:ss"""
|
||||||
|
|
||||||
minutes: int
|
minutes: int
|
||||||
@ -227,13 +233,12 @@ def normalise_track(path):
|
|||||||
|
|
||||||
# Check type
|
# Check type
|
||||||
ftype = os.path.splitext(path)[1][1:]
|
ftype = os.path.splitext(path)[1][1:]
|
||||||
if ftype not in ['mp3', 'flac']:
|
if ftype not in ["mp3", "flac"]:
|
||||||
log.info(
|
log.info(
|
||||||
f"helpers.normalise_track({path}): "
|
f"helpers.normalise_track({path}): " f"File type {ftype} not implemented"
|
||||||
f"File type {ftype} not implemented"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
bitrate = mediainfo(path)['bit_rate']
|
bitrate = mediainfo(path)["bit_rate"]
|
||||||
audio = get_audio_segment(path)
|
audio = get_audio_segment(path)
|
||||||
if not audio:
|
if not audio:
|
||||||
return
|
return
|
||||||
@ -245,23 +250,20 @@ def normalise_track(path):
|
|||||||
_, temp_path = tempfile.mkstemp()
|
_, temp_path = tempfile.mkstemp()
|
||||||
shutil.copyfile(path, temp_path)
|
shutil.copyfile(path, temp_path)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.debug(
|
log.debug(f"helpers.normalise_track({path}): err1: {repr(err)}")
|
||||||
f"helpers.normalise_track({path}): err1: {repr(err)}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Overwrite original file with normalised output
|
# Overwrite original file with normalised output
|
||||||
normalised = effects.normalize(audio)
|
normalised = effects.normalize(audio)
|
||||||
try:
|
try:
|
||||||
normalised.export(path, format=os.path.splitext(path)[1][1:],
|
normalised.export(path, format=os.path.splitext(path)[1][1:], bitrate=bitrate)
|
||||||
bitrate=bitrate)
|
|
||||||
# Fix up permssions and ownership
|
# Fix up permssions and ownership
|
||||||
os.chown(path, stats.st_uid, stats.st_gid)
|
os.chown(path, stats.st_uid, stats.st_gid)
|
||||||
os.chmod(path, stats.st_mode)
|
os.chmod(path, stats.st_mode)
|
||||||
# Copy tags
|
# Copy tags
|
||||||
if ftype == 'flac':
|
if ftype == "flac":
|
||||||
tag_handler = FLAC
|
tag_handler = FLAC
|
||||||
elif ftype == 'mp3':
|
elif ftype == "mp3":
|
||||||
tag_handler = MP3
|
tag_handler = MP3
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
@ -271,9 +273,7 @@ def normalise_track(path):
|
|||||||
dst[tag] = src[tag]
|
dst[tag] = src[tag]
|
||||||
dst.save()
|
dst.save()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.debug(
|
log.debug(f"helpers.normalise_track({path}): err2: {repr(err)}")
|
||||||
f"helpers.normalise_track({path}): err2: {repr(err)}"
|
|
||||||
)
|
|
||||||
# Restore original file
|
# Restore original file
|
||||||
shutil.copyfile(path, temp_path)
|
shutil.copyfile(path, temp_path)
|
||||||
finally:
|
finally:
|
||||||
@ -296,9 +296,9 @@ def open_in_audacity(path: str) -> bool:
|
|||||||
if not path:
|
if not path:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
|
to_pipe: str = "/tmp/audacity_script_pipe.to." + str(os.getuid())
|
||||||
from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
|
from_pipe: str = "/tmp/audacity_script_pipe.from." + str(os.getuid())
|
||||||
eol: str = '\n'
|
eol: str = "\n"
|
||||||
|
|
||||||
def send_command(command: str) -> None:
|
def send_command(command: str) -> None:
|
||||||
"""Send a single command."""
|
"""Send a single command."""
|
||||||
@ -308,13 +308,13 @@ def open_in_audacity(path: str) -> bool:
|
|||||||
def get_response() -> str:
|
def get_response() -> str:
|
||||||
"""Return the command response."""
|
"""Return the command response."""
|
||||||
|
|
||||||
result: str = ''
|
result: str = ""
|
||||||
line: str = ''
|
line: str = ""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
result += line
|
result += line
|
||||||
line = from_audacity.readline()
|
line = from_audacity.readline()
|
||||||
if line == '\n' and len(result) > 0:
|
if line == "\n" and len(result) > 0:
|
||||||
break
|
break
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -325,8 +325,7 @@ def open_in_audacity(path: str) -> bool:
|
|||||||
response = get_response()
|
response = get_response()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
with open(to_pipe, 'w') as to_audacity, open(
|
with open(to_pipe, "w") as to_audacity, open(from_pipe, "rt") as from_audacity:
|
||||||
from_pipe, 'rt') as from_audacity:
|
|
||||||
do_command(f'Import2: Filename="{path}"')
|
do_command(f'Import2: Filename="{path}"')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -338,18 +337,18 @@ def set_track_metadata(session, track):
|
|||||||
t = get_tags(track.path)
|
t = get_tags(track.path)
|
||||||
audio = get_audio_segment(track.path)
|
audio = get_audio_segment(track.path)
|
||||||
|
|
||||||
track.title = t['title']
|
track.title = t["title"]
|
||||||
track.artist = t['artist']
|
track.artist = t["artist"]
|
||||||
track.bitrate = t['bitrate']
|
track.bitrate = t["bitrate"]
|
||||||
|
|
||||||
if not audio:
|
if not audio:
|
||||||
return
|
return
|
||||||
track.duration = len(audio)
|
track.duration = len(audio)
|
||||||
track.start_gap = leading_silence(audio)
|
track.start_gap = leading_silence(audio)
|
||||||
track.fade_at = round(fade_point(audio) / 1000,
|
track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
track.silence_at = (
|
||||||
track.silence_at = round(trailing_silence(audio) / 1000,
|
round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
)
|
||||||
track.mtime = os.path.getmtime(track.path)
|
track.mtime = os.path.getmtime(track.path)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -358,20 +357,20 @@ def set_track_metadata(session, track):
|
|||||||
def show_OK(parent: QMainWindow, title: str, msg: str) -> None:
|
def show_OK(parent: QMainWindow, title: str, msg: str) -> None:
|
||||||
"""Display a message to user"""
|
"""Display a message to user"""
|
||||||
|
|
||||||
QMessageBox.information(parent, title, msg,
|
QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok)
|
||||||
buttons=QMessageBox.StandardButton.Ok)
|
|
||||||
|
|
||||||
|
|
||||||
def show_warning(parent: QMainWindow, title: str, msg: str) -> None:
|
def show_warning(parent: QMainWindow, title: str, msg: str) -> None:
|
||||||
"""Display a warning to user"""
|
"""Display a warning to user"""
|
||||||
|
|
||||||
QMessageBox.warning(parent, title, msg,
|
QMessageBox.warning(parent, title, msg, buttons=QMessageBox.StandardButton.Cancel)
|
||||||
buttons=QMessageBox.StandardButton.Cancel)
|
|
||||||
|
|
||||||
|
|
||||||
def trailing_silence(
|
def trailing_silence(
|
||||||
audio_segment: AudioSegment, silence_threshold: int = -50,
|
audio_segment: AudioSegment,
|
||||||
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
|
silence_threshold: int = -50,
|
||||||
|
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
|
||||||
|
) -> int:
|
||||||
"""Return fade point from start in milliseconds"""
|
"""Return fade point from start in milliseconds"""
|
||||||
|
|
||||||
return fade_point(audio_segment, silence_threshold, chunk_size)
|
return fade_point(audio_segment, silence_threshold, chunk_size)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from slugify import slugify # type: ignore
|
from slugify import slugify # type: ignore
|
||||||
from typing import Dict, Optional
|
from typing import Dict
|
||||||
from PyQt6.QtCore import QUrl # type: ignore
|
from PyQt6.QtCore import QUrl # type: ignore
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWidgets import QTabWidget
|
from PyQt6.QtWidgets import QTabWidget
|
||||||
from config import Config
|
from config import Config
|
||||||
@ -47,13 +47,11 @@ class InfoTabs(QTabWidget):
|
|||||||
|
|
||||||
if url in self.tabtitles.values():
|
if url in self.tabtitles.values():
|
||||||
self.setCurrentIndex(
|
self.setCurrentIndex(
|
||||||
list(self.tabtitles.keys())[
|
list(self.tabtitles.keys())[list(self.tabtitles.values()).index(url)]
|
||||||
list(self.tabtitles.values()).index(url)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
short_title = title[:Config.INFO_TAB_TITLE_LENGTH]
|
short_title = title[: Config.INFO_TAB_TITLE_LENGTH]
|
||||||
|
|
||||||
if self.count() < Config.MAX_INFO_TABS:
|
if self.count() < Config.MAX_INFO_TABS:
|
||||||
# Create a new tab
|
# Create a new tab
|
||||||
@ -63,9 +61,7 @@ class InfoTabs(QTabWidget):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Reuse oldest widget
|
# Reuse oldest widget
|
||||||
widget = min(
|
widget = min(self.last_update, key=self.last_update.get) # type: ignore
|
||||||
self.last_update, key=self.last_update.get # type: ignore
|
|
||||||
)
|
|
||||||
tab_index = self.indexOf(widget)
|
tab_index = self.indexOf(widget)
|
||||||
self.setTabText(tab_index, short_title)
|
self.setTabText(tab_index, short_title)
|
||||||
|
|
||||||
|
|||||||
318
app/models.py
318
app/models.py
@ -1,13 +1,11 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os.path
|
|
||||||
import re
|
import re
|
||||||
import stackprinter # type: ignore
|
|
||||||
|
|
||||||
from dbconfig import Session, scoped_session
|
from dbconfig import scoped_session
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Iterable, List, Optional, Union, ValuesView
|
from typing import List, Optional
|
||||||
|
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
|
||||||
@ -35,21 +33,14 @@ from sqlalchemy.orm.exc import (
|
|||||||
from sqlalchemy.exc import (
|
from sqlalchemy.exc import (
|
||||||
IntegrityError,
|
IntegrityError,
|
||||||
)
|
)
|
||||||
from config import Config
|
|
||||||
from helpers import (
|
|
||||||
fade_point,
|
|
||||||
get_audio_segment,
|
|
||||||
get_tags,
|
|
||||||
leading_silence,
|
|
||||||
trailing_silence,
|
|
||||||
)
|
|
||||||
from log import log
|
from log import log
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
class Carts(Base):
|
class Carts(Base):
|
||||||
__tablename__ = 'carts'
|
__tablename__ = "carts"
|
||||||
|
|
||||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
cart_number: int = Column(Integer, nullable=False, unique=True)
|
cart_number: int = Column(Integer, nullable=False, unique=True)
|
||||||
@ -64,10 +55,15 @@ class Carts(Base):
|
|||||||
f"name={self.name}, path={self.path}>"
|
f"name={self.name}, path={self.path}>"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, session: scoped_session, cart_number: int,
|
def __init__(
|
||||||
name: Optional[str] = None,
|
self,
|
||||||
duration: Optional[int] = None, path: Optional[str] = None,
|
session: scoped_session,
|
||||||
enabled: bool = True) -> None:
|
cart_number: int,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
duration: Optional[int] = None,
|
||||||
|
path: Optional[str] = None,
|
||||||
|
enabled: bool = True,
|
||||||
|
) -> None:
|
||||||
"""Create new cart"""
|
"""Create new cart"""
|
||||||
|
|
||||||
self.cart_number = cart_number
|
self.cart_number = cart_number
|
||||||
@ -81,7 +77,7 @@ class Carts(Base):
|
|||||||
|
|
||||||
|
|
||||||
class NoteColours(Base):
|
class NoteColours(Base):
|
||||||
__tablename__ = 'notecolours'
|
__tablename__ = "notecolours"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
substring = Column(String(256), index=False)
|
substring = Column(String(256), index=False)
|
||||||
@ -106,11 +102,15 @@ class NoteColours(Base):
|
|||||||
if not text:
|
if not text:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for rec in session.execute(
|
for rec in (
|
||||||
|
session.execute(
|
||||||
select(NoteColours)
|
select(NoteColours)
|
||||||
.filter(NoteColours.enabled.is_(True))
|
.filter(NoteColours.enabled.is_(True))
|
||||||
.order_by(NoteColours.order)
|
.order_by(NoteColours.order)
|
||||||
).scalars().all():
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
):
|
||||||
if rec.is_regex:
|
if rec.is_regex:
|
||||||
flags = re.UNICODE
|
flags = re.UNICODE
|
||||||
if not rec.is_casesensitive:
|
if not rec.is_casesensitive:
|
||||||
@ -130,11 +130,11 @@ class NoteColours(Base):
|
|||||||
|
|
||||||
|
|
||||||
class Playdates(Base):
|
class Playdates(Base):
|
||||||
__tablename__ = 'playdates'
|
__tablename__ = "playdates"
|
||||||
|
|
||||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
lastplayed = Column(DateTime, index=True, default=None)
|
lastplayed = Column(DateTime, index=True, default=None)
|
||||||
track_id = Column(Integer, ForeignKey('tracks.id'))
|
track_id = Column(Integer, ForeignKey("tracks.id"))
|
||||||
track: "Tracks" = relationship("Tracks", back_populates="playdates")
|
track: "Tracks" = relationship("Tracks", back_populates="playdates")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@ -152,8 +152,7 @@ class Playdates(Base):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def last_played(session: scoped_session,
|
def last_played(session: scoped_session, track_id: int) -> Optional[datetime]:
|
||||||
track_id: int) -> Optional[datetime]:
|
|
||||||
"""Return datetime track last played or None"""
|
"""Return datetime track last played or None"""
|
||||||
|
|
||||||
last_played = session.execute(
|
last_played = session.execute(
|
||||||
@ -169,8 +168,7 @@ class Playdates(Base):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def played_after(session: scoped_session,
|
def played_after(session: scoped_session, since: datetime) -> List["Playdates"]:
|
||||||
since: datetime) -> List["Playdates"]:
|
|
||||||
"""Return a list of Playdates objects since passed time"""
|
"""Return a list of Playdates objects since passed time"""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -203,7 +201,7 @@ class Playlists(Base):
|
|||||||
"PlaylistRows",
|
"PlaylistRows",
|
||||||
back_populates="playlist",
|
back_populates="playlist",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
order_by="PlaylistRows.plr_rownum"
|
order_by="PlaylistRows.plr_rownum",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@ -232,11 +230,9 @@ class Playlists(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_playlist_from_template(cls,
|
def create_playlist_from_template(
|
||||||
session: scoped_session,
|
cls, session: scoped_session, template: "Playlists", playlist_name: str
|
||||||
template: "Playlists",
|
) -> Optional["Playlists"]:
|
||||||
playlist_name: str) \
|
|
||||||
-> Optional["Playlists"]:
|
|
||||||
"""Create a new playlist from template"""
|
"""Create a new playlist from template"""
|
||||||
|
|
||||||
playlist = cls(session, playlist_name)
|
playlist = cls(session, playlist_name)
|
||||||
@ -277,9 +273,7 @@ class Playlists(Base):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
session.execute(
|
session.execute(
|
||||||
select(cls)
|
select(cls).filter(cls.is_template.is_(True)).order_by(cls.name)
|
||||||
.filter(cls.is_template.is_(True))
|
|
||||||
.order_by(cls.name)
|
|
||||||
)
|
)
|
||||||
.scalars()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
@ -295,7 +289,7 @@ class Playlists(Base):
|
|||||||
.filter(
|
.filter(
|
||||||
cls.tab.is_(None),
|
cls.tab.is_(None),
|
||||||
cls.is_template.is_(False),
|
cls.is_template.is_(False),
|
||||||
cls.deleted.is_(False)
|
cls.deleted.is_(False),
|
||||||
)
|
)
|
||||||
.order_by(cls.last_used.desc())
|
.order_by(cls.last_used.desc())
|
||||||
)
|
)
|
||||||
@ -310,11 +304,7 @@ class Playlists(Base):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
session.execute(
|
session.execute(select(cls).where(cls.tab.is_not(None)).order_by(cls.tab))
|
||||||
select(cls)
|
|
||||||
.where(cls.tab.is_not(None))
|
|
||||||
.order_by(cls.tab)
|
|
||||||
)
|
|
||||||
.scalars()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@ -329,15 +319,9 @@ class Playlists(Base):
|
|||||||
def move_tab(session: scoped_session, frm: int, to: int) -> None:
|
def move_tab(session: scoped_session, frm: int, to: int) -> None:
|
||||||
"""Move tabs"""
|
"""Move tabs"""
|
||||||
|
|
||||||
row_frm = session.execute(
|
row_frm = session.execute(select(Playlists).filter_by(tab=frm)).scalar_one()
|
||||||
select(Playlists)
|
|
||||||
.filter_by(tab=frm)
|
|
||||||
).scalar_one()
|
|
||||||
|
|
||||||
row_to = session.execute(
|
row_to = session.execute(select(Playlists).filter_by(tab=to)).scalar_one()
|
||||||
select(Playlists)
|
|
||||||
.filter_by(tab=to)
|
|
||||||
).scalar_one()
|
|
||||||
|
|
||||||
row_frm.tab = None
|
row_frm.tab = None
|
||||||
row_to.tab = None
|
row_to.tab = None
|
||||||
@ -354,8 +338,9 @@ class Playlists(Base):
|
|||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_as_template(session: scoped_session,
|
def save_as_template(
|
||||||
playlist_id: int, template_name: str) -> None:
|
session: scoped_session, playlist_id: int, template_name: str
|
||||||
|
) -> None:
|
||||||
"""Save passed playlist as new template"""
|
"""Save passed playlist as new template"""
|
||||||
|
|
||||||
template = Playlists(session, template_name)
|
template = Playlists(session, template_name)
|
||||||
@ -369,15 +354,14 @@ class Playlists(Base):
|
|||||||
|
|
||||||
|
|
||||||
class PlaylistRows(Base):
|
class PlaylistRows(Base):
|
||||||
__tablename__ = 'playlist_rows'
|
__tablename__ = "playlist_rows"
|
||||||
|
|
||||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
plr_rownum: int = Column(Integer, nullable=False)
|
plr_rownum: int = Column(Integer, nullable=False)
|
||||||
note: str = Column(String(2048), index=False, default="", nullable=False)
|
note: str = Column(String(2048), index=False, default="", nullable=False)
|
||||||
playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
|
playlist_id: int = Column(Integer, ForeignKey("playlists.id"), nullable=False)
|
||||||
nullable=False)
|
|
||||||
playlist: Playlists = relationship(Playlists, back_populates="rows")
|
playlist: Playlists = relationship(Playlists, back_populates="rows")
|
||||||
track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True)
|
track_id = Column(Integer, ForeignKey("tracks.id"), nullable=True)
|
||||||
track: "Tracks" = relationship("Tracks", back_populates="playlistrows")
|
track: "Tracks" = relationship("Tracks", back_populates="playlistrows")
|
||||||
played: bool = Column(Boolean, nullable=False, index=False, default=False)
|
played: bool = Column(Boolean, nullable=False, index=False, default=False)
|
||||||
|
|
||||||
@ -388,13 +372,14 @@ class PlaylistRows(Base):
|
|||||||
f"note={self.note}, plr_rownum={self.plr_rownum}>"
|
f"note={self.note}, plr_rownum={self.plr_rownum}>"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
session: scoped_session,
|
self,
|
||||||
playlist_id: int,
|
session: scoped_session,
|
||||||
track_id: Optional[int],
|
playlist_id: int,
|
||||||
row_number: int,
|
track_id: Optional[int],
|
||||||
note: str = ""
|
row_number: int,
|
||||||
) -> None:
|
note: str = "",
|
||||||
|
) -> None:
|
||||||
"""Create PlaylistRows object"""
|
"""Create PlaylistRows object"""
|
||||||
|
|
||||||
self.playlist_id = playlist_id
|
self.playlist_id = playlist_id
|
||||||
@ -409,38 +394,38 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
current_note = self.note
|
current_note = self.note
|
||||||
if current_note:
|
if current_note:
|
||||||
self.note = current_note + '\n' + extra_note
|
self.note = current_note + "\n" + extra_note
|
||||||
else:
|
else:
|
||||||
self.note = extra_note
|
self.note = extra_note
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_playlist(session: scoped_session,
|
def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None:
|
||||||
src_id: int,
|
|
||||||
dst_id: int) -> None:
|
|
||||||
"""Copy playlist entries"""
|
"""Copy playlist entries"""
|
||||||
|
|
||||||
src_rows = session.execute(
|
src_rows = (
|
||||||
select(PlaylistRows)
|
session.execute(
|
||||||
.filter(PlaylistRows.playlist_id == src_id)
|
select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id)
|
||||||
).scalars().all()
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
for plr in src_rows:
|
for plr in src_rows:
|
||||||
PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum,
|
PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, plr.note)
|
||||||
plr.note)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_higher_rows(
|
def delete_higher_rows(
|
||||||
session: scoped_session, playlist_id: int, maxrow: int) -> None:
|
session: scoped_session, playlist_id: int, maxrow: int
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Delete rows in given playlist that have a higher row number
|
Delete rows in given playlist that have a higher row number
|
||||||
than 'maxrow'
|
than 'maxrow'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
session.execute(
|
session.execute(
|
||||||
delete(PlaylistRows)
|
delete(PlaylistRows).where(
|
||||||
.where(
|
|
||||||
PlaylistRows.playlist_id == playlist_id,
|
PlaylistRows.playlist_id == playlist_id,
|
||||||
PlaylistRows.plr_rownum > maxrow
|
PlaylistRows.plr_rownum > maxrow,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
session.flush()
|
session.flush()
|
||||||
@ -451,11 +436,15 @@ class PlaylistRows(Base):
|
|||||||
Ensure the row numbers for passed playlist have no gaps
|
Ensure the row numbers for passed playlist have no gaps
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plrs = session.execute(
|
plrs = (
|
||||||
select(PlaylistRows)
|
session.execute(
|
||||||
.where(PlaylistRows.playlist_id == playlist_id)
|
select(PlaylistRows)
|
||||||
.order_by(PlaylistRows.plr_rownum)
|
.where(PlaylistRows.playlist_id == playlist_id)
|
||||||
).scalars().all()
|
.order_by(PlaylistRows.plr_rownum)
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
for i, plr in enumerate(plrs):
|
for i, plr in enumerate(plrs):
|
||||||
plr.plr_rownum = i
|
plr.plr_rownum = i
|
||||||
@ -464,113 +453,126 @@ class PlaylistRows(Base):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_from_id_list(cls, session: scoped_session, playlist_id: int,
|
def get_from_id_list(
|
||||||
plr_ids: List[int]) -> List["PlaylistRows"]:
|
cls, session: scoped_session, playlist_id: int, plr_ids: List[int]
|
||||||
|
) -> List["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
Take a list of PlaylistRows ids and return a list of corresponding
|
Take a list of PlaylistRows ids and return a list of corresponding
|
||||||
PlaylistRows objects
|
PlaylistRows objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plrs = session.execute(
|
plrs = (
|
||||||
select(cls)
|
session.execute(
|
||||||
.where(
|
select(cls)
|
||||||
cls.playlist_id == playlist_id,
|
.where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
|
||||||
cls.id.in_(plr_ids)
|
.order_by(cls.plr_rownum)
|
||||||
).order_by(cls.plr_rownum)).scalars().all()
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_last_used_row(session: scoped_session,
|
def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]:
|
||||||
playlist_id: int) -> Optional[int]:
|
|
||||||
"""Return the last used row for playlist, or None if no rows"""
|
"""Return the last used row for playlist, or None if no rows"""
|
||||||
|
|
||||||
return session.execute(
|
return session.execute(
|
||||||
select(func.max(PlaylistRows.plr_rownum))
|
select(func.max(PlaylistRows.plr_rownum)).where(
|
||||||
.where(PlaylistRows.playlist_id == playlist_id)
|
PlaylistRows.playlist_id == playlist_id
|
||||||
|
)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_track_plr(session: scoped_session, track_id: int,
|
def get_track_plr(
|
||||||
playlist_id: int) -> Optional["PlaylistRows"]:
|
session: scoped_session, track_id: int, playlist_id: int
|
||||||
|
) -> Optional["PlaylistRows"]:
|
||||||
"""Return first matching PlaylistRows object or None"""
|
"""Return first matching PlaylistRows object or None"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
select(PlaylistRows)
|
select(PlaylistRows)
|
||||||
.where(
|
.where(
|
||||||
PlaylistRows.track_id == track_id,
|
PlaylistRows.track_id == track_id,
|
||||||
PlaylistRows.playlist_id == playlist_id
|
PlaylistRows.playlist_id == playlist_id,
|
||||||
)
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_played_rows(cls, session: scoped_session,
|
def get_played_rows(
|
||||||
playlist_id: int) -> List["PlaylistRows"]:
|
cls, session: scoped_session, playlist_id: int
|
||||||
|
) -> List["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
For passed playlist, return a list of rows that
|
For passed playlist, return a list of rows that
|
||||||
have been played.
|
have been played.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plrs = session.execute(
|
plrs = (
|
||||||
select(cls)
|
session.execute(
|
||||||
.where(
|
select(cls)
|
||||||
cls.playlist_id == playlist_id,
|
.where(cls.playlist_id == playlist_id, cls.played.is_(True))
|
||||||
cls.played.is_(True)
|
.order_by(cls.plr_rownum)
|
||||||
)
|
)
|
||||||
.order_by(cls.plr_rownum)
|
.scalars()
|
||||||
).scalars().all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_rows_with_tracks(
|
def get_rows_with_tracks(
|
||||||
cls, session: scoped_session, playlist_id: int,
|
cls,
|
||||||
|
session: scoped_session,
|
||||||
|
playlist_id: int,
|
||||||
from_row: Optional[int] = None,
|
from_row: Optional[int] = None,
|
||||||
to_row: Optional[int] = None) -> List["PlaylistRows"]:
|
to_row: Optional[int] = None,
|
||||||
|
) -> List["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
For passed playlist, return a list of rows that
|
For passed playlist, return a list of rows that
|
||||||
contain tracks
|
contain tracks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = select(cls).where(
|
query = select(cls).where(
|
||||||
cls.playlist_id == playlist_id,
|
cls.playlist_id == playlist_id, cls.track_id.is_not(None)
|
||||||
cls.track_id.is_not(None)
|
|
||||||
)
|
)
|
||||||
if from_row is not None:
|
if from_row is not None:
|
||||||
query = query.where(cls.plr_rownum >= from_row)
|
query = query.where(cls.plr_rownum >= from_row)
|
||||||
if to_row is not None:
|
if to_row is not None:
|
||||||
query = query.where(cls.plr_rownum <= to_row)
|
query = query.where(cls.plr_rownum <= to_row)
|
||||||
|
|
||||||
plrs = (
|
plrs = session.execute((query).order_by(cls.plr_rownum)).scalars().all()
|
||||||
session.execute((query).order_by(cls.plr_rownum)).scalars().all()
|
|
||||||
)
|
|
||||||
|
|
||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_unplayed_rows(cls, session: scoped_session,
|
def get_unplayed_rows(
|
||||||
playlist_id: int) -> List["PlaylistRows"]:
|
cls, session: scoped_session, playlist_id: int
|
||||||
|
) -> List["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
For passed playlist, return a list of playlist rows that
|
For passed playlist, return a list of playlist rows that
|
||||||
have not been played.
|
have not been played.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plrs = session.execute(
|
plrs = (
|
||||||
select(cls)
|
session.execute(
|
||||||
.where(
|
select(cls)
|
||||||
cls.playlist_id == playlist_id,
|
.where(
|
||||||
cls.track_id.is_not(None),
|
cls.playlist_id == playlist_id,
|
||||||
cls.played.is_(False)
|
cls.track_id.is_not(None),
|
||||||
|
cls.played.is_(False),
|
||||||
|
)
|
||||||
|
.order_by(cls.plr_rownum)
|
||||||
)
|
)
|
||||||
.order_by(cls.plr_rownum)
|
.scalars()
|
||||||
).scalars().all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def move_rows_down(session: scoped_session, playlist_id: int,
|
def move_rows_down(
|
||||||
starting_row: int, move_by: int) -> None:
|
session: scoped_session, playlist_id: int, starting_row: int, move_by: int
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create space to insert move_by additional rows by incremented row
|
Create space to insert move_by additional rows by incremented row
|
||||||
number from starting_row to end of playlist
|
number from starting_row to end of playlist
|
||||||
@ -580,7 +582,7 @@ class PlaylistRows(Base):
|
|||||||
update(PlaylistRows)
|
update(PlaylistRows)
|
||||||
.where(
|
.where(
|
||||||
(PlaylistRows.playlist_id == playlist_id),
|
(PlaylistRows.playlist_id == playlist_id),
|
||||||
(PlaylistRows.plr_rownum >= starting_row)
|
(PlaylistRows.plr_rownum >= starting_row),
|
||||||
)
|
)
|
||||||
.values(plr_rownum=PlaylistRows.plr_rownum + move_by)
|
.values(plr_rownum=PlaylistRows.plr_rownum + move_by)
|
||||||
)
|
)
|
||||||
@ -589,7 +591,7 @@ class PlaylistRows(Base):
|
|||||||
class Settings(Base):
|
class Settings(Base):
|
||||||
"""Manage settings"""
|
"""Manage settings"""
|
||||||
|
|
||||||
__tablename__ = 'settings'
|
__tablename__ = "settings"
|
||||||
|
|
||||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
name: str = Column(String(64), nullable=False, unique=True)
|
name: str = Column(String(64), nullable=False, unique=True)
|
||||||
@ -602,21 +604,16 @@ class Settings(Base):
|
|||||||
return f"<Settings(id={self.id}, name={self.name}, {value=}>"
|
return f"<Settings(id={self.id}, name={self.name}, {value=}>"
|
||||||
|
|
||||||
def __init__(self, session: scoped_session, name: str):
|
def __init__(self, session: scoped_session, name: str):
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_int_settings(cls, session: scoped_session,
|
def get_int_settings(cls, session: scoped_session, name: str) -> "Settings":
|
||||||
name: str) -> "Settings":
|
|
||||||
"""Get setting for an integer or return new setting record"""
|
"""Get setting for an integer or return new setting record"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return session.execute(
|
return session.execute(select(cls).where(cls.name == name)).scalar_one()
|
||||||
select(cls)
|
|
||||||
.where(cls.name == name)
|
|
||||||
).scalar_one()
|
|
||||||
|
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
return Settings(session, name)
|
return Settings(session, name)
|
||||||
@ -629,7 +626,7 @@ class Settings(Base):
|
|||||||
|
|
||||||
|
|
||||||
class Tracks(Base):
|
class Tracks(Base):
|
||||||
__tablename__ = 'tracks'
|
__tablename__ = "tracks"
|
||||||
|
|
||||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
title = Column(String(256), index=True)
|
title = Column(String(256), index=True)
|
||||||
@ -641,8 +638,7 @@ class Tracks(Base):
|
|||||||
path: str = Column(String(2048), index=False, nullable=False, unique=True)
|
path: str = Column(String(2048), index=False, nullable=False, unique=True)
|
||||||
mtime = Column(Float, index=True)
|
mtime = Column(Float, index=True)
|
||||||
bitrate = Column(Integer, nullable=True, default=None)
|
bitrate = Column(Integer, nullable=True, default=None)
|
||||||
playlistrows: PlaylistRows = relationship("PlaylistRows",
|
playlistrows: PlaylistRows = relationship("PlaylistRows", back_populates="track")
|
||||||
back_populates="track")
|
|
||||||
playlists = association_proxy("playlistrows", "playlist")
|
playlists = association_proxy("playlistrows", "playlist")
|
||||||
playdates: Playdates = relationship("Playdates", back_populates="track")
|
playdates: Playdates = relationship("Playdates", back_populates="track")
|
||||||
|
|
||||||
@ -653,18 +649,18 @@ class Tracks(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: scoped_session,
|
||||||
path: str,
|
path: str,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
artist: Optional[str] = None,
|
artist: Optional[str] = None,
|
||||||
duration: int = 0,
|
duration: int = 0,
|
||||||
start_gap: int = 0,
|
start_gap: int = 0,
|
||||||
fade_at: Optional[int] = None,
|
fade_at: Optional[int] = None,
|
||||||
silence_at: Optional[int] = None,
|
silence_at: Optional[int] = None,
|
||||||
mtime: Optional[float] = None,
|
mtime: Optional[float] = None,
|
||||||
lastplayed: Optional[datetime] = None,
|
lastplayed: Optional[datetime] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artist = artist
|
self.artist = artist
|
||||||
@ -693,46 +689,36 @@ class Tracks(Base):
|
|||||||
return session.execute(select(cls)).scalars().all()
|
return session.execute(select(cls)).scalars().all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_path(cls, session: scoped_session,
|
def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]:
|
||||||
path: str) -> Optional["Tracks"]:
|
|
||||||
"""
|
"""
|
||||||
Return track with passed path, or None.
|
Return track with passed path, or None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return (
|
return session.execute(
|
||||||
session.execute(
|
select(Tracks).where(Tracks.path == path)
|
||||||
select(Tracks)
|
).scalar_one()
|
||||||
.where(Tracks.path == path)
|
|
||||||
).scalar_one()
|
|
||||||
)
|
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_artists(cls, session: scoped_session,
|
def search_artists(cls, session: scoped_session, text: str) -> List["Tracks"]:
|
||||||
text: str) -> List["Tracks"]:
|
|
||||||
"""Search case-insenstively for artists containing str"""
|
"""Search case-insenstively for artists containing str"""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
session.execute(
|
session.execute(
|
||||||
select(cls)
|
select(cls).where(cls.artist.ilike(f"%{text}%")).order_by(cls.title)
|
||||||
.where(cls.artist.ilike(f"%{text}%"))
|
|
||||||
.order_by(cls.title)
|
|
||||||
)
|
)
|
||||||
.scalars()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_titles(cls, session: scoped_session,
|
def search_titles(cls, session: scoped_session, text: str) -> List["Tracks"]:
|
||||||
text: str) -> List["Tracks"]:
|
|
||||||
"""Search case-insenstively for titles containing str"""
|
"""Search case-insenstively for titles containing str"""
|
||||||
return (
|
return (
|
||||||
session.execute(
|
session.execute(
|
||||||
select(cls)
|
select(cls).where(cls.title.like(f"{text}%")).order_by(cls.title)
|
||||||
.where(cls.title.like(f"{text}%"))
|
|
||||||
.order_by(cls.title)
|
|
||||||
)
|
)
|
||||||
.scalars()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
|
|||||||
13
app/music.py
13
app/music.py
@ -1,16 +1,16 @@
|
|||||||
# import os
|
# import os
|
||||||
import threading
|
import threading
|
||||||
import vlc # type: ignore
|
import vlc # type: ignore
|
||||||
|
|
||||||
#
|
#
|
||||||
from config import Config
|
from config import Config
|
||||||
from datetime import datetime
|
|
||||||
from helpers import file_is_unreadable
|
from helpers import file_is_unreadable
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from log import log
|
from log import log
|
||||||
|
|
||||||
from PyQt6.QtCore import ( # type: ignore
|
from PyQt6.QtCore import ( # type: ignore
|
||||||
QRunnable,
|
QRunnable,
|
||||||
QThreadPool,
|
QThreadPool,
|
||||||
)
|
)
|
||||||
@ -19,7 +19,6 @@ lock = threading.Lock()
|
|||||||
|
|
||||||
|
|
||||||
class FadeTrack(QRunnable):
|
class FadeTrack(QRunnable):
|
||||||
|
|
||||||
def __init__(self, player: vlc.MediaPlayer) -> None:
|
def __init__(self, player: vlc.MediaPlayer) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.player = player
|
self.player = player
|
||||||
@ -47,8 +46,7 @@ class FadeTrack(QRunnable):
|
|||||||
|
|
||||||
for i in range(1, steps + 1):
|
for i in range(1, steps + 1):
|
||||||
measures_to_reduce_by += i
|
measures_to_reduce_by += i
|
||||||
volume_factor = 1 - (
|
volume_factor = 1 - (measures_to_reduce_by / total_measures_count)
|
||||||
measures_to_reduce_by / total_measures_count)
|
|
||||||
self.player.audio_set_volume(int(original_volume * volume_factor))
|
self.player.audio_set_volume(int(original_volume * volume_factor))
|
||||||
sleep(sleep_time)
|
sleep(sleep_time)
|
||||||
|
|
||||||
@ -98,8 +96,7 @@ class Music:
|
|||||||
return None
|
return None
|
||||||
return self.player.get_position()
|
return self.player.get_position()
|
||||||
|
|
||||||
def play(self, path: str,
|
def play(self, path: str, position: Optional[float] = None) -> Optional[int]:
|
||||||
position: Optional[float] = None) -> Optional[int]:
|
|
||||||
"""
|
"""
|
||||||
Start playing the track at path.
|
Start playing the track at path.
|
||||||
|
|
||||||
|
|||||||
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).
|
# parent (eg, bettet bitrate).
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pydymenu # type: ignore
|
import pydymenu # type: ignore
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from helpers import (
|
from helpers import (
|
||||||
fade_point,
|
|
||||||
get_audio_segment,
|
|
||||||
get_tags,
|
get_tags,
|
||||||
leading_silence,
|
|
||||||
trailing_silence,
|
|
||||||
set_track_metadata,
|
set_track_metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,7 +25,7 @@ process_tag_matches = True
|
|||||||
do_processing = True
|
do_processing = True
|
||||||
process_no_matches = True
|
process_no_matches = True
|
||||||
|
|
||||||
source_dir = '/home/kae/music/Singles/tmp'
|
source_dir = "/home/kae/music/Singles/tmp"
|
||||||
parent_dir = os.path.dirname(source_dir)
|
parent_dir = os.path.dirname(source_dir)
|
||||||
# #########################################################
|
# #########################################################
|
||||||
|
|
||||||
@ -46,7 +42,7 @@ def main():
|
|||||||
# We only want to run this against the production database because
|
# We only want to run this against the production database because
|
||||||
# we will affect files in the common pool of tracks used by all
|
# we will affect files in the common pool of tracks used by all
|
||||||
# databases
|
# databases
|
||||||
if 'musicmuster_prod' not in os.environ.get('MM_DB'):
|
if "musicmuster_prod" not in os.environ.get("MM_DB"):
|
||||||
response = input("Not on production database - c to continue: ")
|
response = input("Not on production database - c to continue: ")
|
||||||
if response != "c":
|
if response != "c":
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@ -68,8 +64,8 @@ def main():
|
|||||||
artists_to_path = {}
|
artists_to_path = {}
|
||||||
for k, v in parents.items():
|
for k, v in parents.items():
|
||||||
try:
|
try:
|
||||||
titles_to_path[v['title'].lower()] = k
|
titles_to_path[v["title"].lower()] = k
|
||||||
artists_to_path[v['artist'].lower()] = k
|
artists_to_path[v["artist"].lower()] = k
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -78,44 +74,43 @@ def main():
|
|||||||
if not os.path.isfile(new_path):
|
if not os.path.isfile(new_path):
|
||||||
continue
|
continue
|
||||||
new_tags = get_tags(new_path)
|
new_tags = get_tags(new_path)
|
||||||
new_title = new_tags['title']
|
new_title = new_tags["title"]
|
||||||
new_artist = new_tags['artist']
|
new_artist = new_tags["artist"]
|
||||||
bitrate = new_tags['bitrate']
|
bitrate = new_tags["bitrate"]
|
||||||
|
|
||||||
# If same filename exists in parent direcory, check tags
|
# If same filename exists in parent direcory, check tags
|
||||||
parent_path = os.path.join(parent_dir, new_fname)
|
parent_path = os.path.join(parent_dir, new_fname)
|
||||||
if os.path.exists(parent_path):
|
if os.path.exists(parent_path):
|
||||||
parent_tags = get_tags(parent_path)
|
parent_tags = get_tags(parent_path)
|
||||||
parent_title = parent_tags['title']
|
parent_title = parent_tags["title"]
|
||||||
parent_artist = parent_tags['artist']
|
parent_artist = parent_tags["artist"]
|
||||||
if (
|
if (str(parent_title).lower() == str(new_title).lower()) and (
|
||||||
(str(parent_title).lower() == str(new_title).lower()) and
|
str(parent_artist).lower() == str(new_artist).lower()
|
||||||
(str(parent_artist).lower() == str(new_artist).lower())
|
|
||||||
):
|
):
|
||||||
name_and_tags.append(
|
name_and_tags.append(
|
||||||
f" {new_fname=}, {parent_title} → {new_title}, "
|
f" {new_fname=}, {parent_title} → {new_title}, "
|
||||||
f" {parent_artist} → {new_artist}"
|
f" {parent_artist} → {new_artist}"
|
||||||
)
|
)
|
||||||
if process_name_and_tags_matches:
|
if process_name_and_tags_matches:
|
||||||
process_track(new_path, parent_path, new_title,
|
process_track(new_path, parent_path, new_title, new_artist, bitrate)
|
||||||
new_artist, bitrate)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check for matching tags although filename is different
|
# Check for matching tags although filename is different
|
||||||
if new_title.lower() in titles_to_path:
|
if new_title.lower() in titles_to_path:
|
||||||
possible_path = titles_to_path[new_title.lower()]
|
possible_path = titles_to_path[new_title.lower()]
|
||||||
if parents[possible_path]['artist'].lower() == new_artist.lower():
|
if parents[possible_path]["artist"].lower() == new_artist.lower():
|
||||||
# print(
|
# print(
|
||||||
# f"title={new_title}, artist={new_artist}:\n"
|
# f"title={new_title}, artist={new_artist}:\n"
|
||||||
# f" {new_path} → {parent_path}"
|
# f" {new_path} → {parent_path}"
|
||||||
# )
|
# )
|
||||||
tags_not_name.append(
|
tags_not_name.append(
|
||||||
f"title={new_title}, artist={new_artist}:\n"
|
f"title={new_title}, artist={new_artist}:\n"
|
||||||
f" {new_path} → {parent_path}"
|
f" {new_path} → {parent_path}"
|
||||||
)
|
)
|
||||||
if process_tag_matches:
|
if process_tag_matches:
|
||||||
process_track(new_path, possible_path, new_title,
|
process_track(
|
||||||
new_artist, bitrate)
|
new_path, possible_path, new_title, new_artist, bitrate
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
no_match += 1
|
no_match += 1
|
||||||
@ -132,8 +127,8 @@ def main():
|
|||||||
if choice:
|
if choice:
|
||||||
old_file = os.path.join(parent_dir, choice[0])
|
old_file = os.path.join(parent_dir, choice[0])
|
||||||
oldtags = get_tags(old_file)
|
oldtags = get_tags(old_file)
|
||||||
old_title = oldtags['title']
|
old_title = oldtags["title"]
|
||||||
old_artist = oldtags['artist']
|
old_artist = oldtags["artist"]
|
||||||
print()
|
print()
|
||||||
print(f" File name will change {choice[0]}")
|
print(f" File name will change {choice[0]}")
|
||||||
print(f" → {new_fname}")
|
print(f" → {new_fname}")
|
||||||
@ -232,11 +227,8 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
def process_track(src, dst, title, artist, bitrate):
|
def process_track(src, dst, title, artist, bitrate):
|
||||||
|
|
||||||
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
|
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
|
||||||
print(
|
print(f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n")
|
||||||
f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not do_processing:
|
if not do_processing:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -4,13 +4,7 @@ import os
|
|||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from helpers import (
|
from helpers import (
|
||||||
fade_point,
|
|
||||||
get_audio_segment,
|
|
||||||
get_tags,
|
get_tags,
|
||||||
leading_silence,
|
|
||||||
normalise_track,
|
|
||||||
set_track_metadata,
|
|
||||||
trailing_silence,
|
|
||||||
)
|
)
|
||||||
from log import log
|
from log import log
|
||||||
from models import Tracks
|
from models import Tracks
|
||||||
@ -72,12 +66,14 @@ def check_db(session):
|
|||||||
print("Invalid paths in database")
|
print("Invalid paths in database")
|
||||||
print("-------------------------")
|
print("-------------------------")
|
||||||
for t in paths_not_found:
|
for t in paths_not_found:
|
||||||
print(f"""
|
print(
|
||||||
|
f"""
|
||||||
Track ID: {t.id}
|
Track ID: {t.id}
|
||||||
Path: {t.path}
|
Path: {t.path}
|
||||||
Title: {t.title}
|
Title: {t.title}
|
||||||
Artist: {t.artist}
|
Artist: {t.artist}
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
if more_files_to_report:
|
if more_files_to_report:
|
||||||
print("There were more paths than listed that were not found")
|
print("There were more paths than listed that were not found")
|
||||||
|
|
||||||
|
|||||||
11
conftest.py
11
conftest.py
@ -1,15 +1,15 @@
|
|||||||
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
|
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
|
||||||
sys.path.append("app")
|
# Flake8 doesn't like the sys.append within imports
|
||||||
import models
|
# import sys
|
||||||
|
# sys.path.append("app")
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def connection():
|
def connection():
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
@ -21,7 +21,8 @@ def connection():
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def setup_database(connection):
|
def setup_database(connection):
|
||||||
from app.models import Base # noqa E402
|
from app.models import Base # noqa E402
|
||||||
|
|
||||||
Base.metadata.bind = connection
|
Base.metadata.bind = connection
|
||||||
Base.metadata.create_all()
|
Base.metadata.create_all()
|
||||||
# seed_database()
|
# seed_database()
|
||||||
|
|||||||
1
devnotes.txt
Normal file
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
|
* Database backed
|
||||||
* Can be almost entirely keyboard driven
|
* Can be almost entirely keyboard driven
|
||||||
* Playlist management
|
* Open multiple playlists in tabs
|
||||||
* Easily add new tracks to playlists
|
|
||||||
* Show multiple playlists on tabs
|
|
||||||
* Play tracks from any playlist
|
* Play tracks from any playlist
|
||||||
* Add notes/comments to tracks on playlist
|
* Add notes/comments to tracks on playlist
|
||||||
* Automataic olour-coding of notes/comments according to content
|
* Automatatic colour-coding of notes/comments according to content
|
||||||
* Preview tracks before playing to audience
|
* Preview tracks before playing to audience
|
||||||
* Time of day clock
|
* Time of day clock
|
||||||
* Elapsed track time counter
|
* Elapsed track time counter
|
||||||
@ -42,8 +40,8 @@ Features
|
|||||||
* Time to run until track is silent
|
* Time to run until track is silent
|
||||||
* Graphic of volume from 5 seconds (configurable) before fade until
|
* Graphic of volume from 5 seconds (configurable) before fade until
|
||||||
track is silent
|
track is silent
|
||||||
* Ability to hide played tracks in playlist
|
* Optionally hide played tracks in playlist
|
||||||
* Buttone to drop playout volume by 3dB for talkover
|
* Button to drop playout volume by 3dB for talkover
|
||||||
* Playlist displays:
|
* Playlist displays:
|
||||||
* Title
|
* Title
|
||||||
* Artist
|
* Artist
|
||||||
@ -51,18 +49,18 @@ Features
|
|||||||
* Estimated start time of track
|
* Estimated start time of track
|
||||||
* Estimated end time of track
|
* Estimated end time of track
|
||||||
* When track was last played
|
* When track was last played
|
||||||
* Bits per second (bps bitrate) of track
|
* Bits per second (bitrate) of track
|
||||||
* Length of silence in recording before music starts
|
* Length of leading silence in recording before track starts
|
||||||
* Total track length of arbitrary sections of tracks
|
* Total track length of arbitrary sections of tracks
|
||||||
* Commands that are sent to OBS Studio (eg, for automated scene
|
* Commands that are sent to OBS Studio (eg, for automated scene
|
||||||
changes)
|
changes)
|
||||||
* Playlist templates
|
* Playlist templates
|
||||||
* Move selected/unplayed tracks between playlists
|
* Move selected or unplayed tracks between playlists
|
||||||
* Down CSV of played tracks between arbitrary dates/times
|
* Download CSV of tracks played between arbitrary dates/times
|
||||||
* Search for tracks by title or artist
|
* Search for tracks by title or artist
|
||||||
* Automatic search of current/next track in Wikipedia
|
* Automatic search of current and next track in Wikipedia
|
||||||
* Optional search of selected track in Wikipedia
|
* Optional search of any track in Wikipedia
|
||||||
* Optional search of selected track in Songfacts
|
* Optional search of any track in Songfacts
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
|
|||||||
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">
|
<ul class="simple">
|
||||||
<li><p>Database backed</p></li>
|
<li><p>Database backed</p></li>
|
||||||
<li><p>Can be almost entirely keyboard driven</p></li>
|
<li><p>Can be almost entirely keyboard driven</p></li>
|
||||||
<li><p>Playlist management</p></li>
|
<li><p>Open multiple playlists in tabs</p></li>
|
||||||
<li><p>Easily add new tracks to playlists</p></li>
|
|
||||||
<li><p>Show multiple playlists on tabs</p></li>
|
|
||||||
<li><p>Play tracks from any playlist</p></li>
|
<li><p>Play tracks from any playlist</p></li>
|
||||||
<li><p>Add notes/comments to tracks on playlist</p></li>
|
<li><p>Add notes/comments to tracks on playlist</p></li>
|
||||||
<li><p>Automataic olour-coding of notes/comments according to content</p></li>
|
<li><p>Automatatic colour-coding of notes/comments according to content</p></li>
|
||||||
<li><p>Preview tracks before playing to audience</p></li>
|
<li><p>Preview tracks before playing to audience</p></li>
|
||||||
<li><p>Time of day clock</p></li>
|
<li><p>Time of day clock</p></li>
|
||||||
<li><p>Elapsed track time counter</p></li>
|
<li><p>Elapsed track time counter</p></li>
|
||||||
@ -234,8 +232,8 @@ production of live internet radio shows.</p>
|
|||||||
<li><p>Time to run until track is silent</p></li>
|
<li><p>Time to run until track is silent</p></li>
|
||||||
<li><p>Graphic of volume from 5 seconds (configurable) before fade until
|
<li><p>Graphic of volume from 5 seconds (configurable) before fade until
|
||||||
track is silent</p></li>
|
track is silent</p></li>
|
||||||
<li><p>Ability to hide played tracks in playlist</p></li>
|
<li><p>Optionally hide played tracks in playlist</p></li>
|
||||||
<li><p>Buttone to drop playout volume by 3dB for talkover</p></li>
|
<li><p>Button to drop playout volume by 3dB for talkover</p></li>
|
||||||
<li><dl class="simple">
|
<li><dl class="simple">
|
||||||
<dt>Playlist displays:</dt><dd><ul>
|
<dt>Playlist displays:</dt><dd><ul>
|
||||||
<li><p>Title</p></li>
|
<li><p>Title</p></li>
|
||||||
@ -244,8 +242,8 @@ track is silent</p></li>
|
|||||||
<li><p>Estimated start time of track</p></li>
|
<li><p>Estimated start time of track</p></li>
|
||||||
<li><p>Estimated end time of track</p></li>
|
<li><p>Estimated end time of track</p></li>
|
||||||
<li><p>When track was last played</p></li>
|
<li><p>When track was last played</p></li>
|
||||||
<li><p>Bits per second (bps bitrate) of track</p></li>
|
<li><p>Bits per second (bitrate) of track</p></li>
|
||||||
<li><p>Length of silence in recording before music starts</p></li>
|
<li><p>Length of leading silence in recording before track starts</p></li>
|
||||||
<li><p>Total track length of arbitrary sections of tracks</p></li>
|
<li><p>Total track length of arbitrary sections of tracks</p></li>
|
||||||
<li><p>Commands that are sent to OBS Studio (eg, for automated scene
|
<li><p>Commands that are sent to OBS Studio (eg, for automated scene
|
||||||
changes)</p></li>
|
changes)</p></li>
|
||||||
@ -254,12 +252,12 @@ changes)</p></li>
|
|||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
<li><p>Playlist templates</p></li>
|
<li><p>Playlist templates</p></li>
|
||||||
<li><p>Move selected/unplayed tracks between playlists</p></li>
|
<li><p>Move selected or unplayed tracks between playlists</p></li>
|
||||||
<li><p>Down CSV of played tracks between arbitrary dates/times</p></li>
|
<li><p>Download CSV of tracks played between arbitrary dates/times</p></li>
|
||||||
<li><p>Search for tracks by title or artist</p></li>
|
<li><p>Search for tracks by title or artist</p></li>
|
||||||
<li><p>Automatic search of current/next track in Wikipedia</p></li>
|
<li><p>Automatic search of current and next track in Wikipedia</p></li>
|
||||||
<li><p>Optional search of selected track in Wikipedia</p></li>
|
<li><p>Optional search of any track in Wikipedia</p></li>
|
||||||
<li><p>Optional search of selected track in Songfacts</p></li>
|
<li><p>Optional search of any track in Songfacts</p></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<section id="requirements">
|
<section id="requirements">
|
||||||
|
|||||||
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
|
* Database backed
|
||||||
* Can be almost entirely keyboard driven
|
* Can be almost entirely keyboard driven
|
||||||
* Easily add new tracks to playlists
|
* Open multiple playlists in tabs
|
||||||
* Show multiple playlists on tabs
|
|
||||||
* Play tracks from any playlist
|
* Play tracks from any playlist
|
||||||
* Add notes/comments to tracks on playlist
|
* Add notes/comments to tracks on playlist
|
||||||
* Automataic olour-coding of notes/comments according to content
|
* Automatatic colour-coding of notes/comments according to content
|
||||||
* Preview tracks before playing to audience
|
* Preview tracks before playing to audience
|
||||||
* Time of day clock
|
* Time of day clock
|
||||||
* Elapsed track time counter
|
* Elapsed track time counter
|
||||||
@ -41,8 +40,8 @@ Features
|
|||||||
* Time to run until track is silent
|
* Time to run until track is silent
|
||||||
* Graphic of volume from 5 seconds (configurable) before fade until
|
* Graphic of volume from 5 seconds (configurable) before fade until
|
||||||
track is silent
|
track is silent
|
||||||
* Ability to hide played tracks in playlist
|
* Optionally hide played tracks in playlist
|
||||||
* Buttone to drop playout volume by 3dB for talkover
|
* Button to drop playout volume by 3dB for talkover
|
||||||
* Playlist displays:
|
* Playlist displays:
|
||||||
* Title
|
* Title
|
||||||
* Artist
|
* Artist
|
||||||
@ -50,18 +49,18 @@ Features
|
|||||||
* Estimated start time of track
|
* Estimated start time of track
|
||||||
* Estimated end time of track
|
* Estimated end time of track
|
||||||
* When track was last played
|
* When track was last played
|
||||||
* Bits per second (bps bitrate) of track
|
* Bits per second (bitrate) of track
|
||||||
* Length of silence in recording before music starts
|
* Length of leading silence in recording before track starts
|
||||||
* Total track length of arbitrary sections of tracks
|
* Total track length of arbitrary sections of tracks
|
||||||
* Commands that are sent to OBS Studio (eg, for automated scene
|
* Commands that are sent to OBS Studio (eg, for automated scene
|
||||||
changes)
|
changes)
|
||||||
* Playlist templates
|
* Playlist templates
|
||||||
* Move selected/unplayed tracks between playlists
|
* Move selected or unplayed tracks between playlists
|
||||||
* Down CSV of played tracks between arbitrary dates/times
|
* Download CSV of tracks played between arbitrary dates/times
|
||||||
* Search for tracks by title or artist
|
* Search for tracks by title or artist
|
||||||
* Automatic search of current/next track in Wikipedia
|
* Automatic search of current and next track in Wikipedia
|
||||||
* Optional search of selected track in Wikipedia
|
* Optional search of any track in Wikipedia
|
||||||
* Optional search of selected track in Songfacts
|
* Optional search of any track in Songfacts
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
|
|||||||
802
poetry.lock
generated
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"
|
pudb = "^2022.1.3"
|
||||||
sphinx = "^7.0.1"
|
sphinx = "^7.0.1"
|
||||||
furo = "^2023.5.20"
|
furo = "^2023.5.20"
|
||||||
|
black = "^23.3.0"
|
||||||
|
flakehell = "^0.9.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|||||||
7
test.py
7
test.py
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from PyQt5 import QtGui, QtWidgets
|
from PyQt5 import QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
class TabBar(QtWidgets.QTabBar):
|
class TabBar(QtWidgets.QTabBar):
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
painter = QtWidgets.QStylePainter(self)
|
painter = QtWidgets.QStylePainter(self)
|
||||||
@ -13,16 +14,18 @@ class TabBar(QtWidgets.QTabBar):
|
|||||||
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
|
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
|
||||||
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
|
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
|
||||||
|
|
||||||
|
|
||||||
class Window(QtWidgets.QTabWidget):
|
class Window(QtWidgets.QTabWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QtWidgets.QTabWidget.__init__(self)
|
QtWidgets.QTabWidget.__init__(self)
|
||||||
self.setTabBar(TabBar(self))
|
self.setTabBar(TabBar(self))
|
||||||
for color in 'tomato orange yellow lightgreen skyblue plum'.split():
|
for color in "tomato orange yellow lightgreen skyblue plum".split():
|
||||||
self.addTab(QtWidgets.QWidget(self), color)
|
self.addTab(QtWidgets.QWidget(self), color)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
window = Window()
|
window = Window()
|
||||||
window.resize(420, 200)
|
window.resize(420, 200)
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
from config import Config
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from helpers import *
|
from helpers import (
|
||||||
from models import Tracks
|
fade_point,
|
||||||
|
get_audio_segment,
|
||||||
|
get_tags,
|
||||||
|
get_relative_date,
|
||||||
|
leading_silence,
|
||||||
|
ms_to_mmss,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_fade_point():
|
def test_fade_point():
|
||||||
@ -18,8 +23,8 @@ def test_fade_point():
|
|||||||
testdata = eval(f.read())
|
testdata = eval(f.read())
|
||||||
|
|
||||||
# Volume detection can vary, so ± 1 second is OK
|
# Volume detection can vary, so ± 1 second is OK
|
||||||
assert fade_at < testdata['fade_at'] + 1000
|
assert fade_at < testdata["fade_at"] + 1000
|
||||||
assert fade_at > testdata['fade_at'] - 1000
|
assert fade_at > testdata["fade_at"] - 1000
|
||||||
|
|
||||||
|
|
||||||
def test_get_tags():
|
def test_get_tags():
|
||||||
@ -32,8 +37,8 @@ def test_get_tags():
|
|||||||
with open(test_track_data) as f:
|
with open(test_track_data) as f:
|
||||||
testdata = eval(f.read())
|
testdata = eval(f.read())
|
||||||
|
|
||||||
assert tags['artist'] == testdata['artist']
|
assert tags["artist"] == testdata["artist"]
|
||||||
assert tags['title'] == testdata['title']
|
assert tags["title"] == testdata["title"]
|
||||||
|
|
||||||
|
|
||||||
def test_get_relative_date():
|
def test_get_relative_date():
|
||||||
@ -44,8 +49,7 @@ def test_get_relative_date():
|
|||||||
eight_days_ago = today_at_10 - timedelta(days=8)
|
eight_days_ago = today_at_10 - timedelta(days=8)
|
||||||
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
||||||
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
||||||
assert get_relative_date(
|
assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
||||||
sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
|
||||||
|
|
||||||
|
|
||||||
def test_leading_silence():
|
def test_leading_silence():
|
||||||
@ -62,8 +66,8 @@ def test_leading_silence():
|
|||||||
testdata = eval(f.read())
|
testdata = eval(f.read())
|
||||||
|
|
||||||
# Volume detection can vary, so ± 1 second is OK
|
# Volume detection can vary, so ± 1 second is OK
|
||||||
assert silence_at < testdata['leading_silence'] + 1000
|
assert silence_at < testdata["leading_silence"] + 1000
|
||||||
assert silence_at > testdata['leading_silence'] - 1000
|
assert silence_at > testdata["leading_silence"] - 1000
|
||||||
|
|
||||||
|
|
||||||
def test_ms_to_mmss():
|
def test_ms_to_mmss():
|
||||||
|
|||||||
@ -135,7 +135,6 @@ def test_playdates_remove_track(session):
|
|||||||
track_path = "/a/b/c"
|
track_path = "/a/b/c"
|
||||||
track = Tracks(session, track_path)
|
track = Tracks(session, track_path)
|
||||||
|
|
||||||
playdate = Playdates(session, track.id)
|
|
||||||
Playdates.remove_track(session, track.id)
|
Playdates.remove_track(session, track.id)
|
||||||
|
|
||||||
last_played = Playdates.last_played(session, track.id)
|
last_played = Playdates.last_played(session, track.id)
|
||||||
@ -149,10 +148,8 @@ def test_playlist_create(session):
|
|||||||
|
|
||||||
def test_playlist_add_note(session):
|
def test_playlist_add_note(session):
|
||||||
note_text = "my note"
|
note_text = "my note"
|
||||||
note_row = 2
|
|
||||||
|
|
||||||
playlist = Playlists(session, "my playlist")
|
playlist = Playlists(session, "my playlist")
|
||||||
note = playlist.add_note(session, note_row, note_text)
|
|
||||||
|
|
||||||
assert len(playlist.notes) == 1
|
assert len(playlist.notes) == 1
|
||||||
playlist_note = playlist.notes[0]
|
playlist_note = playlist.notes[0]
|
||||||
@ -356,9 +353,7 @@ def test_tracks_get_all_paths(session):
|
|||||||
def test_tracks_get_all_tracks(session):
|
def test_tracks_get_all_tracks(session):
|
||||||
# Need two tracks
|
# Need two tracks
|
||||||
track1_path = "/a/b/c"
|
track1_path = "/a/b/c"
|
||||||
track1 = Tracks(session, track1_path)
|
|
||||||
track2_path = "/m/n/o"
|
track2_path = "/m/n/o"
|
||||||
track2 = Tracks(session, track2_path)
|
|
||||||
|
|
||||||
result = Tracks.get_all_tracks(session)
|
result = Tracks.get_all_tracks(session)
|
||||||
assert track1_path in [a.path for a in result]
|
assert track1_path in [a.path for a in result]
|
||||||
@ -369,9 +364,7 @@ def test_tracks_by_filename(session):
|
|||||||
track1_path = "/a/b/c"
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
track1 = Tracks(session, track1_path)
|
track1 = Tracks(session, track1_path)
|
||||||
assert Tracks.get_by_filename(
|
assert Tracks.get_by_filename(session, os.path.basename(track1_path)) is track1
|
||||||
session, os.path.basename(track1_path)
|
|
||||||
) is track1
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_by_path(session):
|
def test_tracks_by_path(session):
|
||||||
@ -403,26 +396,24 @@ def test_tracks_rescan(session):
|
|||||||
# Re-read the track
|
# Re-read the track
|
||||||
track_read = Tracks.get_by_path(session, test_track_path)
|
track_read = Tracks.get_by_path(session, test_track_path)
|
||||||
|
|
||||||
assert track_read.duration == testdata['duration']
|
assert track_read.duration == testdata["duration"]
|
||||||
assert track_read.start_gap == testdata['leading_silence']
|
assert track_read.start_gap == testdata["leading_silence"]
|
||||||
# Silence detection can vary, so ± 1 second is OK
|
# Silence detection can vary, so ± 1 second is OK
|
||||||
assert track_read.fade_at < testdata['fade_at'] + 1000
|
assert track_read.fade_at < testdata["fade_at"] + 1000
|
||||||
assert track_read.fade_at > testdata['fade_at'] - 1000
|
assert track_read.fade_at > testdata["fade_at"] - 1000
|
||||||
assert track_read.silence_at < testdata['trailing_silence'] + 1000
|
assert track_read.silence_at < testdata["trailing_silence"] + 1000
|
||||||
assert track_read.silence_at > testdata['trailing_silence'] - 1000
|
assert track_read.silence_at > testdata["trailing_silence"] - 1000
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_remove_by_path(session):
|
def test_tracks_remove_by_path(session):
|
||||||
track1_path = "/a/b/c"
|
track1_path = "/a/b/c"
|
||||||
|
|
||||||
track1 = Tracks(session, track1_path)
|
|
||||||
assert len(Tracks.get_all_tracks(session)) == 1
|
assert len(Tracks.get_all_tracks(session)) == 1
|
||||||
Tracks.remove_by_path(session, track1_path)
|
Tracks.remove_by_path(session, track1_path)
|
||||||
assert len(Tracks.get_all_tracks(session)) == 0
|
assert len(Tracks.get_all_tracks(session)) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_search_artists(session):
|
def test_tracks_search_artists(session):
|
||||||
|
|
||||||
track1_path = "/a/b/c"
|
track1_path = "/a/b/c"
|
||||||
track1_artist = "Artist One"
|
track1_artist = "Artist One"
|
||||||
track1 = Tracks(session, track1_path)
|
track1 = Tracks(session, track1_path)
|
||||||
@ -435,7 +426,6 @@ def test_tracks_search_artists(session):
|
|||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
x = Tracks.get_all_tracks(session)
|
|
||||||
artist_first_word = track1_artist.split()[0].lower()
|
artist_first_word = track1_artist.split()[0].lower()
|
||||||
assert len(Tracks.search_artists(session, artist_first_word)) == 2
|
assert len(Tracks.search_artists(session, artist_first_word)) == 2
|
||||||
assert len(Tracks.search_artists(session, track1_artist)) == 1
|
assert len(Tracks.search_artists(session, track1_artist)) == 1
|
||||||
@ -454,7 +444,6 @@ def test_tracks_search_titles(session):
|
|||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
x = Tracks.get_all_tracks(session)
|
|
||||||
title_first_word = track1_title.split()[0].lower()
|
title_first_word = track1_title.split()[0].lower()
|
||||||
assert len(Tracks.search_titles(session, title_first_word)) == 2
|
assert len(Tracks.search_titles(session, title_first_word)) == 2
|
||||||
assert len(Tracks.search_titles(session, track1_title)) == 1
|
assert len(Tracks.search_titles(session, track1_title)) == 1
|
||||||
|
|||||||
@ -3,7 +3,6 @@ from PyQt5.QtCore import Qt
|
|||||||
from app import playlists
|
from app import playlists
|
||||||
from app import models
|
from app import models
|
||||||
from app import musicmuster
|
from app import musicmuster
|
||||||
from app import dbconfig
|
|
||||||
|
|
||||||
|
|
||||||
def seed2tracks(session):
|
def seed2tracks(session):
|
||||||
@ -78,7 +77,6 @@ def test_save_and_restore(qtbot, session):
|
|||||||
|
|
||||||
|
|
||||||
def test_meta_all_clear(qtbot, session):
|
def test_meta_all_clear(qtbot, session):
|
||||||
|
|
||||||
# Create playlist
|
# Create playlist
|
||||||
playlist = models.Playlists(session, "my playlist")
|
playlist = models.Playlists(session, "my playlist")
|
||||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
@ -107,7 +105,6 @@ def test_meta_all_clear(qtbot, session):
|
|||||||
|
|
||||||
|
|
||||||
def test_meta(qtbot, session):
|
def test_meta(qtbot, session):
|
||||||
|
|
||||||
# Create playlist
|
# Create playlist
|
||||||
playlist = playlists.Playlists(session, "my playlist")
|
playlist = playlists.Playlists(session, "my playlist")
|
||||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
@ -141,7 +138,7 @@ def test_meta(qtbot, session):
|
|||||||
|
|
||||||
# Add a note
|
# Add a note
|
||||||
note_text = "my 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)
|
note = models.Notes(session, playlist.id, note_row, note_text)
|
||||||
playlist_tab._insert_note(session, note)
|
playlist_tab._insert_note(session, note)
|
||||||
|
|
||||||
@ -211,7 +208,6 @@ def test_clear_next(qtbot, session):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_selected_row(qtbot, monkeypatch, session):
|
def test_get_selected_row(qtbot, monkeypatch, session):
|
||||||
|
|
||||||
monkeypatch.setattr(musicmuster, "Session", session)
|
monkeypatch.setattr(musicmuster, "Session", session)
|
||||||
monkeypatch.setattr(playlists, "Session", session)
|
monkeypatch.setattr(playlists, "Session", session)
|
||||||
|
|
||||||
@ -236,15 +232,12 @@ def test_get_selected_row(qtbot, monkeypatch, session):
|
|||||||
row0_item0 = playlist_tab.item(0, 0)
|
row0_item0 = playlist_tab.item(0, 0)
|
||||||
assert row0_item0 is not None
|
assert row0_item0 is not None
|
||||||
rect = playlist_tab.visualItemRect(row0_item0)
|
rect = playlist_tab.visualItemRect(row0_item0)
|
||||||
qtbot.mouseClick(
|
qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
|
||||||
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
|
|
||||||
)
|
|
||||||
row_number = playlist_tab.get_selected_row()
|
row_number = playlist_tab.get_selected_row()
|
||||||
assert row_number == 0
|
assert row_number == 0
|
||||||
|
|
||||||
|
|
||||||
def test_set_next(qtbot, monkeypatch, session):
|
def test_set_next(qtbot, monkeypatch, session):
|
||||||
|
|
||||||
monkeypatch.setattr(musicmuster, "Session", session)
|
monkeypatch.setattr(musicmuster, "Session", session)
|
||||||
monkeypatch.setattr(playlists, "Session", session)
|
monkeypatch.setattr(playlists, "Session", session)
|
||||||
seed2tracks(session)
|
seed2tracks(session)
|
||||||
@ -274,14 +267,11 @@ def test_set_next(qtbot, monkeypatch, session):
|
|||||||
row0_item2 = playlist_tab.item(0, 2)
|
row0_item2 = playlist_tab.item(0, 2)
|
||||||
assert row0_item2 is not None
|
assert row0_item2 is not None
|
||||||
rect = playlist_tab.visualItemRect(row0_item2)
|
rect = playlist_tab.visualItemRect(row0_item2)
|
||||||
qtbot.mouseClick(
|
qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
|
||||||
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
|
|
||||||
)
|
|
||||||
selected_title = playlist_tab.get_selected_title()
|
selected_title = playlist_tab.get_selected_title()
|
||||||
assert selected_title == track1_title
|
assert selected_title == track1_title
|
||||||
|
|
||||||
qtbot.keyPress(playlist_tab.viewport(), "N",
|
qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier)
|
||||||
modifier=Qt.ControlModifier)
|
|
||||||
qtbot.wait(1000)
|
qtbot.wait(1000)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
tree.py
24
tree.py
@ -9,9 +9,10 @@ datas = {
|
|||||||
],
|
],
|
||||||
"No Category": [
|
"No Category": [
|
||||||
("New Game", "Playnite", "", "", "Never", "Not Plated", ""),
|
("New Game", "Playnite", "", "", "Never", "Not Plated", ""),
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class GroupDelegate(QtWidgets.QStyledItemDelegate):
|
class GroupDelegate(QtWidgets.QStyledItemDelegate):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(GroupDelegate, self).__init__(parent)
|
super(GroupDelegate, self).__init__(parent)
|
||||||
@ -25,6 +26,7 @@ class GroupDelegate(QtWidgets.QStyledItemDelegate):
|
|||||||
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
|
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
|
||||||
option.icon = self._minus_icon if is_open else self._plus_icon
|
option.icon = self._minus_icon if is_open else self._plus_icon
|
||||||
|
|
||||||
|
|
||||||
class GroupView(QtWidgets.QTreeView):
|
class GroupView(QtWidgets.QTreeView):
|
||||||
def __init__(self, model, parent=None):
|
def __init__(self, model, parent=None):
|
||||||
super(GroupView, self).__init__(parent)
|
super(GroupView, self).__init__(parent)
|
||||||
@ -48,7 +50,18 @@ class GroupModel(QtGui.QStandardItemModel):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(GroupModel, self).__init__(parent)
|
super(GroupModel, self).__init__(parent)
|
||||||
self.setColumnCount(8)
|
self.setColumnCount(8)
|
||||||
self.setHorizontalHeaderLabels(["", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""])
|
self.setHorizontalHeaderLabels(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"Name",
|
||||||
|
"Library",
|
||||||
|
"Release Date",
|
||||||
|
"Genre(s)",
|
||||||
|
"Last Played",
|
||||||
|
"Time Played",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
for i in range(self.columnCount()):
|
for i in range(self.columnCount()):
|
||||||
it = self.horizontalHeaderItem(i)
|
it = self.horizontalHeaderItem(i)
|
||||||
it.setForeground(QtGui.QColor("#F2F2F2"))
|
it.setForeground(QtGui.QColor("#F2F2F2"))
|
||||||
@ -84,7 +97,7 @@ class GroupModel(QtGui.QStandardItemModel):
|
|||||||
item.setEditable(False)
|
item.setEditable(False)
|
||||||
item.setBackground(QtGui.QColor("#0D1225"))
|
item.setBackground(QtGui.QColor("#0D1225"))
|
||||||
item.setForeground(QtGui.QColor("#F2F2F2"))
|
item.setForeground(QtGui.QColor("#F2F2F2"))
|
||||||
group_item.setChild(j, i+1, item)
|
group_item.setChild(j, i + 1, item)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow):
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
@ -100,11 +113,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
for children in childrens:
|
for children in childrens:
|
||||||
model.append_element_to_group(group_item, children)
|
model.append_element_to_group(group_item, children)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
w = MainWindow()
|
w = MainWindow()
|
||||||
w.resize(720, 240)
|
w.resize(720, 240)
|
||||||
w.show()
|
w.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|||||||
1
web.py
1
web.py
@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import sys
|
import sys
|
||||||
from pprint import pprint
|
|
||||||
from PyQt6.QtWidgets import QApplication, QLabel
|
from PyQt6.QtWidgets import QApplication, QLabel
|
||||||
from PyQt6.QtGui import QColor, QPalette
|
from PyQt6.QtGui import QColor, QPalette
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user