Compare commits
No commits in common. "a8fad358b9f147219d8bb1c66933eb739a8fc910" and "a46b9a3d6f27ec87d6ca87b5a71f386bb5115498" have entirely different histories.
a8fad358b9
...
a46b9a3d6f
8
.envrc
8
.envrc
@ -4,17 +4,17 @@ export MAIL_PORT=587
|
|||||||
export MAIL_SERVER="smtp.fastmail.com"
|
export MAIL_SERVER="smtp.fastmail.com"
|
||||||
export MAIL_USERNAME="kae@midnighthax.com"
|
export MAIL_USERNAME="kae@midnighthax.com"
|
||||||
export MAIL_USE_TLS=True
|
export MAIL_USE_TLS=True
|
||||||
|
export PYGAME_HIDE_SUPPORT_PROMPT=1
|
||||||
branch=$(git branch --show-current)
|
branch=$(git branch --show-current)
|
||||||
|
|
||||||
# Always treat running from /home/kae/mm as production
|
# Always treat running from /home/kae/mm as production
|
||||||
if [ $(pwd) == /home/kae/mm ]; then
|
if [ $(pwd) == /home/kae/mm ]; then
|
||||||
export MM_ENV="PRODUCTION"
|
export MM_ENV="PRODUCTION"
|
||||||
export DATABASE_URL="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
||||||
elif on_git_branch master; then
|
elif on_git_branch master; then
|
||||||
export MM_ENV="PRODUCTION"
|
export MM_ENV="PRODUCTION"
|
||||||
export DATABASE_URL="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
||||||
else
|
else
|
||||||
export MM_ENV="DEVELOPMENT"
|
export MM_ENV="DEVELOPMENT"
|
||||||
export DATABASE_URL="mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster"
|
export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster"
|
||||||
export PYTHONBREAKPOINT="pudb.set_trace"
|
export PYTHONBREAKPOINT="pudb.set_trace"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -42,6 +42,7 @@ class MusicMusterSignals(QObject):
|
|||||||
end_reset_model_signal = pyqtSignal(int)
|
end_reset_model_signal = pyqtSignal(int)
|
||||||
next_track_changed_signal = pyqtSignal()
|
next_track_changed_signal = pyqtSignal()
|
||||||
resize_rows_signal = pyqtSignal(int)
|
resize_rows_signal = pyqtSignal(int)
|
||||||
|
row_order_changed_signal = pyqtSignal(int)
|
||||||
search_songfacts_signal = pyqtSignal(str)
|
search_songfacts_signal = pyqtSignal(str)
|
||||||
search_wikipedia_signal = pyqtSignal(str)
|
search_wikipedia_signal = pyqtSignal(str)
|
||||||
show_warning_signal = pyqtSignal(str, str)
|
show_warning_signal = pyqtSignal(str, str)
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
# Standard library imports
|
|
||||||
|
|
||||||
# PyQt imports
|
|
||||||
|
|
||||||
# Third party imports
|
|
||||||
from alchemical import Alchemical # type:ignore
|
|
||||||
|
|
||||||
# App imports
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
|
||||||
"""
|
|
||||||
Singleton class to ensure we only ever have one db object
|
|
||||||
"""
|
|
||||||
|
|
||||||
__instance = None
|
|
||||||
|
|
||||||
def __init__(self, database_url, **kwargs):
|
|
||||||
if DatabaseManager.__instance is None:
|
|
||||||
self.db = Alchemical(database_url, **kwargs)
|
|
||||||
self.db.create_all()
|
|
||||||
DatabaseManager.__instance = self
|
|
||||||
else:
|
|
||||||
raise Exception("Attempted to create a second DatabaseManager instance")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_instance(database_url, **kwargs):
|
|
||||||
if DatabaseManager.__instance is None:
|
|
||||||
DatabaseManager(database_url, **kwargs)
|
|
||||||
return DatabaseManager.__instance
|
|
||||||
@ -57,7 +57,7 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
# 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
|
||||||
dburi = os.environ.get("DATABASE_URL")
|
dburi = os.environ.get("ALCHEMICAL_DATABASE_URI")
|
||||||
if not dburi or "musicmuster_prod" not in dburi:
|
if not dburi or "musicmuster_prod" not in dburi:
|
||||||
if not ask_yes_no(
|
if not ask_yes_no(
|
||||||
"Not production database",
|
"Not production database",
|
||||||
@ -84,12 +84,7 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
continue
|
continue
|
||||||
rf = TrackFileData(new_file_path=new_file_path)
|
rf = TrackFileData(new_file_path=new_file_path)
|
||||||
rf.tags = get_tags(new_file_path)
|
rf.tags = get_tags(new_file_path)
|
||||||
if not (
|
if not rf.tags["title"] or not rf.tags["artist"]:
|
||||||
"title" in rf.tags
|
|
||||||
and "artist" in rf.tags
|
|
||||||
and rf.tags["title"]
|
|
||||||
and rf.tags["artist"]
|
|
||||||
):
|
|
||||||
show_warning(
|
show_warning(
|
||||||
parent=self.main_window,
|
parent=self.main_window,
|
||||||
title="Error",
|
title="Error",
|
||||||
@ -182,9 +177,7 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
for cbbn in candidates_by_basename:
|
for cbbn in candidates_by_basename:
|
||||||
cbbn_tags = get_tags(cbbn.path)
|
cbbn_tags = get_tags(cbbn.path)
|
||||||
if (
|
if (
|
||||||
"title" in cbbn_tags
|
cbbn_tags["title"].lower() == new_path_title.lower()
|
||||||
and cbbn_tags["title"].lower() == new_path_title.lower()
|
|
||||||
and "artist" in cbbn_tags
|
|
||||||
and cbbn_tags["artist"].lower() == new_path_artist.lower()
|
and cbbn_tags["artist"].lower() == new_path_artist.lower()
|
||||||
):
|
):
|
||||||
match_track = cbbn
|
match_track = cbbn
|
||||||
@ -204,13 +197,10 @@ class ReplaceFilesDialog(QDialog):
|
|||||||
if candidates_by_title:
|
if candidates_by_title:
|
||||||
# Check artist tag
|
# Check artist tag
|
||||||
for cbt in candidates_by_title:
|
for cbt in candidates_by_title:
|
||||||
try:
|
cbt_artist = get_tags(cbt.path)["artist"]
|
||||||
cbt_artist = get_tags(cbt.path)["artist"]
|
if cbt_artist.lower() == new_path_artist.lower():
|
||||||
if cbt_artist.lower() == new_path_artist.lower():
|
match_track = cbt
|
||||||
match_track = cbt
|
break
|
||||||
break
|
|
||||||
except FileNotFoundError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return match_track
|
return match_track
|
||||||
|
|
||||||
@ -260,9 +250,9 @@ class TrackSelectDialog(QDialog):
|
|||||||
self.track: Optional[Tracks] = None
|
self.track: Optional[Tracks] = None
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
|
|
||||||
record = Settings.get_setting(self.session, "dbdialog_width")
|
record = Settings.get_int_settings(self.session, "dbdialog_width")
|
||||||
width = record.f_int or 800
|
width = record.f_int or 800
|
||||||
record = Settings.get_setting(self.session, "dbdialog_height")
|
record = Settings.get_int_settings(self.session, "dbdialog_height")
|
||||||
height = record.f_int or 600
|
height = record.f_int or 600
|
||||||
self.resize(width, height)
|
self.resize(width, height)
|
||||||
|
|
||||||
@ -376,13 +366,13 @@ class TrackSelectDialog(QDialog):
|
|||||||
if not event:
|
if not event:
|
||||||
return
|
return
|
||||||
|
|
||||||
record = Settings.get_setting(self.session, "dbdialog_height")
|
record = Settings.get_int_settings(self.session, "dbdialog_height")
|
||||||
record.f_int = self.height()
|
if record.f_int != self.height():
|
||||||
|
record.update(self.session, {"f_int": self.height()})
|
||||||
|
|
||||||
record = Settings.get_setting(self.session, "dbdialog_width")
|
record = Settings.get_int_settings(self.session, "dbdialog_width")
|
||||||
record.f_int = self.width()
|
if record.f_int != self.width():
|
||||||
|
record.update(self.session, {"f_int": self.width()})
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|||||||
@ -118,8 +118,11 @@ def get_embedded_time(text: str) -> Optional[dt.datetime]:
|
|||||||
def get_all_track_metadata(filepath: str) -> Dict[str, str | int | float]:
|
def get_all_track_metadata(filepath: str) -> Dict[str, str | int | float]:
|
||||||
"""Return all track metadata"""
|
"""Return all track metadata"""
|
||||||
|
|
||||||
return get_audio_metadata(filepath) | get_tags(filepath) | dict(path=filepath)
|
return (
|
||||||
|
get_audio_metadata(filepath)
|
||||||
|
| get_tags(filepath)
|
||||||
|
| dict(path=filepath)
|
||||||
|
)
|
||||||
|
|
||||||
def get_audio_metadata(filepath: str) -> Dict[str, str | int | float]:
|
def get_audio_metadata(filepath: str) -> Dict[str, str | int | float]:
|
||||||
"""Return audio metadata"""
|
"""Return audio metadata"""
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import sys
|
|||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
from alchemical import Alchemical # type:ignore
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
bindparam,
|
bindparam,
|
||||||
delete,
|
delete,
|
||||||
@ -21,20 +22,18 @@ from sqlalchemy.orm import joinedload
|
|||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from dbmanager import DatabaseManager
|
|
||||||
import dbtables
|
import dbtables
|
||||||
from config import Config
|
from config import Config
|
||||||
from log import log
|
from log import log
|
||||||
|
|
||||||
|
|
||||||
# Establish database connection
|
# Establish database connection
|
||||||
DATABASE_URL = os.environ.get("DATABASE_URL")
|
ALCHEMICAL_DATABASE_URI = os.environ.get("ALCHEMICAL_DATABASE_URI")
|
||||||
if DATABASE_URL is None:
|
if ALCHEMICAL_DATABASE_URI is None:
|
||||||
raise ValueError("DATABASE_URL is undefined")
|
raise ValueError("ALCHEMICAL_DATABASE_URI is undefined")
|
||||||
if "unittest" in sys.modules and "sqlite" not in DATABASE_URL:
|
if "unittest" in sys.modules and "sqlite" not in ALCHEMICAL_DATABASE_URI:
|
||||||
raise ValueError("Unit tests running on non-Sqlite database")
|
raise ValueError("Unit tests running on non-Sqlite database")
|
||||||
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
db = Alchemical(ALCHEMICAL_DATABASE_URI, engine_options=Config.ENGINE_OPTIONS)
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
@ -585,8 +584,22 @@ class Settings(dbtables.SettingsTable):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting(cls, session: Session, name: str) -> "Settings":
|
def all_as_dict(cls, session):
|
||||||
"""Get existing setting or return new setting record"""
|
"""
|
||||||
|
Return all setting in a dictionary keyed by name
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
settings = session.scalars(select(cls)).all()
|
||||||
|
for setting in settings:
|
||||||
|
result[setting.name] = setting
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_int_settings(cls, session: Session, name: str) -> "Settings":
|
||||||
|
"""Get setting for an integer or return new setting record"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return session.execute(select(cls).where(cls.name == name)).scalar_one()
|
return session.execute(select(cls).where(cls.name == name)).scalar_one()
|
||||||
@ -594,6 +607,12 @@ class Settings(dbtables.SettingsTable):
|
|||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
return Settings(session, name)
|
return Settings(session, name)
|
||||||
|
|
||||||
|
def update(self, session: Session, data: dict) -> None:
|
||||||
|
for key, value in data.items():
|
||||||
|
assert hasattr(self, key)
|
||||||
|
setattr(self, key, value)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
class Tracks(dbtables.TracksTable):
|
class Tracks(dbtables.TracksTable):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@ -239,6 +239,36 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
|
settings = Settings.all_as_dict(session)
|
||||||
|
record = settings["mainwindow_height"]
|
||||||
|
if record.f_int != self.height():
|
||||||
|
record.update(session, {"f_int": self.height()})
|
||||||
|
|
||||||
|
record = settings["mainwindow_width"]
|
||||||
|
if record.f_int != self.width():
|
||||||
|
record.update(session, {"f_int": self.width()})
|
||||||
|
|
||||||
|
record = settings["mainwindow_x"]
|
||||||
|
if record.f_int != self.x():
|
||||||
|
record.update(session, {"f_int": self.x()})
|
||||||
|
|
||||||
|
record = settings["mainwindow_y"]
|
||||||
|
if record.f_int != self.y():
|
||||||
|
record.update(session, {"f_int": self.y()})
|
||||||
|
|
||||||
|
# Save splitter settings
|
||||||
|
splitter_sizes = self.splitter.sizes()
|
||||||
|
assert len(splitter_sizes) == 2
|
||||||
|
splitter_top, splitter_bottom = splitter_sizes
|
||||||
|
|
||||||
|
record = settings["splitter_top"]
|
||||||
|
if record.f_int != splitter_top:
|
||||||
|
record.update(session, {"f_int": splitter_top})
|
||||||
|
|
||||||
|
record = settings["splitter_bottom"]
|
||||||
|
if record.f_int != splitter_bottom:
|
||||||
|
record.update(session, {"f_int": splitter_bottom})
|
||||||
|
|
||||||
# Save tab number of open playlists
|
# Save tab number of open playlists
|
||||||
open_playlist_ids: dict[int, int] = {}
|
open_playlist_ids: dict[int, int] = {}
|
||||||
for idx in range(self.tabPlaylist.count()):
|
for idx in range(self.tabPlaylist.count()):
|
||||||
@ -250,20 +280,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
log.debug(f"Set {playlist=} tab to {idx=}")
|
log.debug(f"Set {playlist=} tab to {idx=}")
|
||||||
playlist.tab = idx
|
playlist.tab = idx
|
||||||
|
|
||||||
# Save window attributes
|
# Save current tab
|
||||||
splitter_top, splitter_bottom = self.splitter.sizes()
|
record = settings["active_tab"]
|
||||||
attributes_to_save = dict(
|
record.update(session, {"f_int": self.tabPlaylist.currentIndex()})
|
||||||
mainwindow_height=self.height(),
|
|
||||||
mainwindow_width=self.width(),
|
|
||||||
mainwindow_x=self.x(),
|
|
||||||
mainwindow_y=self.y(),
|
|
||||||
splitter_top=splitter_top,
|
|
||||||
splitter_bottom=splitter_bottom,
|
|
||||||
active_tab=self.tabPlaylist.currentIndex(),
|
|
||||||
)
|
|
||||||
for name, value in attributes_to_save.items():
|
|
||||||
record = Settings.get_setting(session, name)
|
|
||||||
record.f_int = value
|
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@ -756,7 +775,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
playlist_ids.append(playlist.id)
|
playlist_ids.append(playlist.id)
|
||||||
log.debug(f"load_last_playlists() loaded {playlist=}")
|
log.debug(f"load_last_playlists() loaded {playlist=}")
|
||||||
# Set active tab
|
# Set active tab
|
||||||
record = Settings.get_setting(session, "active_tab")
|
record = Settings.get_int_settings(session, "active_tab")
|
||||||
if record.f_int is not None and record.f_int >= 0:
|
if record.f_int is not None and record.f_int >= 0:
|
||||||
self.tabPlaylist.setCurrentIndex(record.f_int)
|
self.tabPlaylist.setCurrentIndex(record.f_int)
|
||||||
|
|
||||||
@ -1315,17 +1334,40 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
"""Set size of window from database"""
|
"""Set size of window from database"""
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
x = Settings.get_setting(session, "mainwindow_x").f_int or 100
|
settings = Settings.all_as_dict(session)
|
||||||
y = Settings.get_setting(session, "mainwindow_y").f_int or 100
|
if "mainwindow_x" in settings:
|
||||||
width = Settings.get_setting(session, "mainwindow_width").f_int or 100
|
record = settings["mainwindow_x"]
|
||||||
height = Settings.get_setting(session, "mainwindow_height").f_int or 100
|
x = record.f_int or 1
|
||||||
splitter_top = Settings.get_setting(session, "splitter_top").f_int or 100
|
else:
|
||||||
splitter_bottom = (
|
x = 100
|
||||||
Settings.get_setting(session, "splitter_bottom").f_int or 100
|
if "mainwindow_y" in settings:
|
||||||
)
|
record = settings["mainwindow_y"]
|
||||||
|
y = record.f_int or 1
|
||||||
|
else:
|
||||||
|
y = 100
|
||||||
|
if "mainwindow_width" in settings:
|
||||||
|
record = settings["mainwindow_width"]
|
||||||
|
width = record.f_int or 1599
|
||||||
|
else:
|
||||||
|
width = 100
|
||||||
|
if "mainwindow_height" in settings:
|
||||||
|
record = settings["mainwindow_height"]
|
||||||
|
height = record.f_int or 981
|
||||||
|
else:
|
||||||
|
height = 100
|
||||||
self.setGeometry(x, y, width, height)
|
self.setGeometry(x, y, width, height)
|
||||||
|
if "splitter_top" in settings:
|
||||||
|
record = settings["splitter_top"]
|
||||||
|
splitter_top = record.f_int or 256
|
||||||
|
else:
|
||||||
|
splitter_top = 100
|
||||||
|
if "splitter_bottom" in settings:
|
||||||
|
record = settings["splitter_bottom"]
|
||||||
|
splitter_bottom = record.f_int or 256
|
||||||
|
else:
|
||||||
|
splitter_bottom = 100
|
||||||
self.splitter.setSizes([splitter_top, splitter_bottom])
|
self.splitter.setSizes([splitter_top, splitter_bottom])
|
||||||
|
return
|
||||||
|
|
||||||
def set_selected_track_next(self) -> None:
|
def set_selected_track_next(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1619,9 +1661,11 @@ class SelectPlaylistDialog(QDialog):
|
|||||||
self.session = session
|
self.session = session
|
||||||
self.playlist = None
|
self.playlist = None
|
||||||
|
|
||||||
record = Settings.get_setting(self.session, "select_playlist_dialog_width")
|
record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
|
||||||
width = record.f_int or 800
|
width = record.f_int or 800
|
||||||
record = Settings.get_setting(self.session, "select_playlist_dialog_height")
|
record = Settings.get_int_settings(
|
||||||
|
self.session, "select_playlist_dialog_height"
|
||||||
|
)
|
||||||
height = record.f_int or 600
|
height = record.f_int or 600
|
||||||
self.resize(width, height)
|
self.resize(width, height)
|
||||||
|
|
||||||
@ -1632,13 +1676,15 @@ class SelectPlaylistDialog(QDialog):
|
|||||||
self.ui.lstPlaylists.addItem(p)
|
self.ui.lstPlaylists.addItem(p)
|
||||||
|
|
||||||
def __del__(self): # review
|
def __del__(self): # review
|
||||||
record = Settings.get_setting(self.session, "select_playlist_dialog_height")
|
record = Settings.get_int_settings(
|
||||||
record.f_int = self.height()
|
self.session, "select_playlist_dialog_height"
|
||||||
|
)
|
||||||
|
if record.f_int != self.height():
|
||||||
|
record.update(self.session, {"f_int": self.height()})
|
||||||
|
|
||||||
record = Settings.get_setting(self.session, "select_playlist_dialog_width")
|
record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
|
||||||
record.f_int = self.width()
|
if record.f_int != self.width():
|
||||||
|
record.update(self.session, {"f_int": self.width()})
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
def list_doubleclick(self, entry): # review
|
def list_doubleclick(self, entry): # review
|
||||||
self.playlist = entry.data(Qt.ItemDataRole.UserRole)
|
self.playlist = entry.data(Qt.ItemDataRole.UserRole)
|
||||||
|
|||||||
@ -127,6 +127,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
self.signals.begin_reset_model_signal.connect(self.begin_reset_model)
|
self.signals.begin_reset_model_signal.connect(self.begin_reset_model)
|
||||||
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
||||||
|
self.signals.row_order_changed_signal.connect(self.row_order_changed)
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
# Ensure row numbers in playlist are contiguous
|
# Ensure row numbers in playlist are contiguous
|
||||||
@ -975,6 +976,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
# Reset of model must come after session has been closed
|
# Reset of model must come after session has been closed
|
||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
|
self.signals.row_order_changed_signal.emit(to_playlist_id)
|
||||||
self.signals.end_reset_model_signal.emit(to_playlist_id)
|
self.signals.end_reset_model_signal.emit(to_playlist_id)
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
|
|
||||||
@ -1128,12 +1130,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
def reset_track_sequence_row_numbers(self) -> None:
|
def reset_track_sequence_row_numbers(self) -> None:
|
||||||
"""
|
"""
|
||||||
Signal handler for when row ordering has changed.
|
Signal handler for when row ordering has changed
|
||||||
|
|
||||||
Example: row 4 is marked as next. Row 2 is deleted. The PlaylistRows table will
|
|
||||||
be correctly updated with change of row number, but track_sequence.next will still
|
|
||||||
contain row_number==4. This function fixes up the track_sequence row numbers by
|
|
||||||
looking up the plr_id and retrieving the row number from the database.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.debug("reset_track_sequence_row_numbers()")
|
log.debug("reset_track_sequence_row_numbers()")
|
||||||
@ -1141,15 +1138,20 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Check the track_sequence next, current and previous plrs and
|
# Check the track_sequence next, current and previous plrs and
|
||||||
# update the row number
|
# update the row number
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
for ts in [track_sequence.next, track_sequence.current, track_sequence.previous]:
|
if track_sequence.next and track_sequence.next.row_number:
|
||||||
if ts and ts.row_number:
|
next_plr = session.get(PlaylistRows, track_sequence.next.row_number)
|
||||||
plr = session.get(PlaylistRows, ts.plr_id)
|
if next_plr:
|
||||||
if plr and plr.plr_rownum != ts.row_number:
|
track_sequence.next.row_number = next_plr.plr_rownum
|
||||||
log.error(
|
if track_sequence.current and track_sequence.current.row_number:
|
||||||
"reset_track_sequence_row_numbers: "
|
now_plr = session.get(PlaylistRows, track_sequence.current.row_number)
|
||||||
f"from {ts=} to {plr.plr_rownum=}"
|
if now_plr:
|
||||||
)
|
track_sequence.current.row_number = now_plr.plr_rownum
|
||||||
ts.row_number = plr.plr_rownum
|
if track_sequence.previous and track_sequence.previous.row_number:
|
||||||
|
previous_plr = session.get(
|
||||||
|
PlaylistRows, track_sequence.previous.row_number
|
||||||
|
)
|
||||||
|
if previous_plr:
|
||||||
|
track_sequence.previous.row_number = previous_plr.plr_rownum
|
||||||
|
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
|
|
||||||
@ -1189,6 +1191,19 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return len(self.playlist_rows)
|
return len(self.playlist_rows)
|
||||||
|
|
||||||
|
def row_order_changed(self, playlist_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Signal handler for when row ordering has changed
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.debug(f"row_order_changed({playlist_id=}) {self.playlist_id=}")
|
||||||
|
|
||||||
|
# Only action if this is for us
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.reset_track_sequence_row_numbers()
|
||||||
|
|
||||||
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
|
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if the selection is sortable. That means:
|
Return True if the selection is sortable. That means:
|
||||||
|
|||||||
@ -573,7 +573,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
attr_name = f"playlist_col_{column_number}_width"
|
attr_name = f"playlist_col_{column_number}_width"
|
||||||
record = Settings.get_setting(session, attr_name)
|
record = Settings.get_int_settings(session, attr_name)
|
||||||
record.f_int = self.columnWidth(column_number)
|
record.f_int = self.columnWidth(column_number)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@ -900,7 +900,7 @@ class PlaylistTab(QTableView):
|
|||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
for column_number in range(header.count() - 1):
|
for column_number in range(header.count() - 1):
|
||||||
attr_name = f"playlist_col_{column_number}_width"
|
attr_name = f"playlist_col_{column_number}_width"
|
||||||
record = Settings.get_setting(session, attr_name)
|
record = Settings.get_int_settings(session, attr_name)
|
||||||
if record.f_int is not None:
|
if record.f_int is not None:
|
||||||
self.setColumnWidth(column_number, record.f_int)
|
self.setColumnWidth(column_number, record.f_int)
|
||||||
else:
|
else:
|
||||||
|
|||||||
272
app/replace_files.py
Executable file
272
app/replace_files.py
Executable file
@ -0,0 +1,272 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Script to replace existing files in parent directory. Typical usage:
|
||||||
|
# the current directory contains a "better" version of the file than the
|
||||||
|
# parent (eg, bettet bitrate).
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import pydymenu # type: ignore
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from helpers import (
|
||||||
|
get_tags,
|
||||||
|
set_track_metadata,
|
||||||
|
)
|
||||||
|
from models import db, Tracks
|
||||||
|
|
||||||
|
# ###################### SETTINGS #########################
|
||||||
|
process_name_and_tags_matches = True
|
||||||
|
process_tag_matches = True
|
||||||
|
do_processing = True
|
||||||
|
process_no_matches = True
|
||||||
|
|
||||||
|
source_dir = "/home/kae/music/Singles/tmp"
|
||||||
|
parent_dir = os.path.dirname(source_dir)
|
||||||
|
# #########################################################
|
||||||
|
|
||||||
|
name_and_tags: List[str] = []
|
||||||
|
tags_not_name: List[str] = []
|
||||||
|
# multiple_similar: List[str] = []
|
||||||
|
# possibles: List[str] = []
|
||||||
|
no_match: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global no_match
|
||||||
|
|
||||||
|
# We only want to run this against the production database because
|
||||||
|
# we will affect files in the common pool of tracks used by all
|
||||||
|
# databases
|
||||||
|
if "musicmuster_prod" not in os.environ.get("ALCHEMICAL_DATABASE_URI"):
|
||||||
|
response = input("Not on production database - c to continue: ")
|
||||||
|
if response != "c":
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
assert source_dir != parent_dir
|
||||||
|
|
||||||
|
# Scan parent directory
|
||||||
|
with db.Session() as session:
|
||||||
|
all_tracks = Tracks.get_all(session)
|
||||||
|
parent_tracks = [a for a in all_tracks if parent_dir in a.path]
|
||||||
|
parent_fnames = [os.path.basename(a.path) for a in parent_tracks]
|
||||||
|
# Create a dictionary of parent paths with their titles and
|
||||||
|
# artists
|
||||||
|
parents = {}
|
||||||
|
for t in parent_tracks:
|
||||||
|
parents[t.path] = {"title": t.title, "artist": t.artist}
|
||||||
|
titles_to_path = {}
|
||||||
|
artists_to_path = {}
|
||||||
|
for k, v in parents.items():
|
||||||
|
try:
|
||||||
|
titles_to_path[v["title"].lower()] = k
|
||||||
|
artists_to_path[v["artist"].lower()] = k
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for new_fname in os.listdir(source_dir):
|
||||||
|
new_path = os.path.join(source_dir, new_fname)
|
||||||
|
if not os.path.isfile(new_path):
|
||||||
|
continue
|
||||||
|
new_tags = get_tags(new_path)
|
||||||
|
new_title = new_tags["title"]
|
||||||
|
if not new_title:
|
||||||
|
print(f"{new_fname} does not have a title tag")
|
||||||
|
sys.exit(1)
|
||||||
|
new_artist = new_tags["artist"]
|
||||||
|
if not new_artist:
|
||||||
|
print(f"{new_fname} does not have an artist tag")
|
||||||
|
sys.exit(1)
|
||||||
|
bitrate = new_tags["bitrate"]
|
||||||
|
|
||||||
|
# If same filename exists in parent direcory, check tags
|
||||||
|
parent_path = os.path.join(parent_dir, new_fname)
|
||||||
|
if os.path.exists(parent_path):
|
||||||
|
parent_tags = get_tags(parent_path)
|
||||||
|
parent_title = parent_tags["title"]
|
||||||
|
parent_artist = parent_tags["artist"]
|
||||||
|
if (str(parent_title).lower() == str(new_title).lower()) and (
|
||||||
|
str(parent_artist).lower() == str(new_artist).lower()
|
||||||
|
):
|
||||||
|
name_and_tags.append(
|
||||||
|
f" {new_fname=}, {parent_title} → {new_title}, "
|
||||||
|
f" {parent_artist} → {new_artist}"
|
||||||
|
)
|
||||||
|
if process_name_and_tags_matches:
|
||||||
|
process_track(new_path, parent_path, new_title, new_artist, bitrate)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for matching tags although filename is different
|
||||||
|
if new_title.lower() in titles_to_path:
|
||||||
|
possible_path = titles_to_path[new_title.lower()]
|
||||||
|
if parents[possible_path]["artist"].lower() == new_artist.lower():
|
||||||
|
# print(
|
||||||
|
# f"title={new_title}, artist={new_artist}:\n"
|
||||||
|
# f" {new_path} → {parent_path}"
|
||||||
|
# )
|
||||||
|
tags_not_name.append(
|
||||||
|
f"title={new_title}, artist={new_artist}:\n"
|
||||||
|
f" {new_path} → {parent_path}"
|
||||||
|
)
|
||||||
|
if process_tag_matches:
|
||||||
|
process_track(
|
||||||
|
new_path, possible_path, new_title, new_artist, bitrate
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
no_match += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
no_match += 1
|
||||||
|
|
||||||
|
# Try to find a near match
|
||||||
|
|
||||||
|
if process_no_matches:
|
||||||
|
prompt = f"file={new_fname}\n title={new_title}\n artist={new_artist}: "
|
||||||
|
# Use fzf to search
|
||||||
|
choice = pydymenu.rofi(parent_fnames, prompt=prompt)
|
||||||
|
if choice:
|
||||||
|
old_file = os.path.join(parent_dir, choice[0])
|
||||||
|
oldtags = get_tags(old_file)
|
||||||
|
old_title = oldtags["title"]
|
||||||
|
old_artist = oldtags["artist"]
|
||||||
|
print()
|
||||||
|
print(f" File name will change {choice[0]}")
|
||||||
|
print(f" → {new_fname}")
|
||||||
|
print()
|
||||||
|
print(f" Title tag will change {old_title}")
|
||||||
|
print(f" → {new_title}")
|
||||||
|
print()
|
||||||
|
print(f" Artist tag will change {old_artist}")
|
||||||
|
print(f" → {new_artist}")
|
||||||
|
print()
|
||||||
|
data = input("Go ahead (y to accept)? ")
|
||||||
|
if data == "y":
|
||||||
|
process_track(new_path, old_file, new_title, new_artist, bitrate)
|
||||||
|
continue
|
||||||
|
if data == "q":
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
# else:
|
||||||
|
# no_match.append(f"{new_fname}, {new_title=}, {new_artist=}")
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# if match_count > 1:
|
||||||
|
# multiple_similar.append(new_fname + "\n " + "\n ".join(matches))
|
||||||
|
# if match_count <= 26 and process_multiple_matches:
|
||||||
|
# print(f"\n file={new_fname}\n title={new_title}\n artist={new_artist}\n")
|
||||||
|
# d = {}
|
||||||
|
# while True:
|
||||||
|
# for i, match in enumerate(matches):
|
||||||
|
# d[i] = match
|
||||||
|
# for k, v in d.items():
|
||||||
|
# print(f"{k}: {v}")
|
||||||
|
# data = input("pick one, return to quit: ")
|
||||||
|
# if data == "":
|
||||||
|
# break
|
||||||
|
# try:
|
||||||
|
# key = int(data)
|
||||||
|
# except ValueError:
|
||||||
|
# continue
|
||||||
|
# if key in d:
|
||||||
|
# dst = d[key]
|
||||||
|
# process_track(new_path, dst, new_title, new_artist, bitrate)
|
||||||
|
# break
|
||||||
|
# else:
|
||||||
|
# continue
|
||||||
|
# continue # from break after testing for "" in data
|
||||||
|
# # One match, check tags
|
||||||
|
# sim_name = matches[0]
|
||||||
|
# p = get_tags(sim_name)
|
||||||
|
# parent_title = p['title']
|
||||||
|
# parent_artist = p['artist']
|
||||||
|
# if (
|
||||||
|
# (str(parent_title).lower() != str(new_title).lower()) or
|
||||||
|
# (str(parent_artist).lower() != str(new_artist).lower())
|
||||||
|
# ):
|
||||||
|
# possibles.append(
|
||||||
|
# f"File: {os.path.basename(sim_name)} → {new_fname}"
|
||||||
|
# f"\n {parent_title} → {new_title}\n {parent_artist} → {new_artist}"
|
||||||
|
# )
|
||||||
|
# process_track(new_path, sim_name, new_title, new_artist, bitrate)
|
||||||
|
# continue
|
||||||
|
# tags_not_name.append(f"Rename {os.path.basename(sim_name)} → {new_fname}")
|
||||||
|
# process_track(new_path, sim_name, new_title, new_artist, bitrate)
|
||||||
|
|
||||||
|
print(f"Name and tags match ({len(name_and_tags)}):")
|
||||||
|
# print(" \n".join(name_and_tags))
|
||||||
|
# print()
|
||||||
|
|
||||||
|
# print(f"Name but not tags match ({len(name_not_tags)}):")
|
||||||
|
# print(" \n".join(name_not_tags))
|
||||||
|
# print()
|
||||||
|
|
||||||
|
print(f"Tags but not name match ({len(tags_not_name)}):")
|
||||||
|
# print(" \n".join(tags_not_name))
|
||||||
|
# print()
|
||||||
|
|
||||||
|
# print(f"Multiple similar names ({len(multiple_similar)}):")
|
||||||
|
# print(" \n".join(multiple_similar))
|
||||||
|
# print()
|
||||||
|
|
||||||
|
# print(f"Possibles: ({len(possibles)}):")
|
||||||
|
# print(" \n".join(possibles))
|
||||||
|
# print()
|
||||||
|
|
||||||
|
# print(f"No match ({len(no_match)}):")
|
||||||
|
# print(" \n".join(no_match))
|
||||||
|
# print()
|
||||||
|
|
||||||
|
# print(f"Name and tags match ({len(name_and_tags)}):")
|
||||||
|
# print(f"Name but not tags match ({len(name_not_tags)}):")
|
||||||
|
# print(f"Tags but not name match ({len(tags_not_name)}):")
|
||||||
|
# print(f"Multiple similar names ({len(multiple_similar)}):")
|
||||||
|
# print(f"Possibles: ({len(possibles)}):")
|
||||||
|
# print(f"No match ({len(no_match)}):")
|
||||||
|
print(f"No matches: {no_match}")
|
||||||
|
|
||||||
|
|
||||||
|
def process_track(src, dst, title, artist, bitrate):
|
||||||
|
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
|
||||||
|
print(f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n")
|
||||||
|
|
||||||
|
if not do_processing:
|
||||||
|
return
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
track = Tracks.get_by_path(session, dst)
|
||||||
|
if track:
|
||||||
|
# Update path, but workaround MariaDB bug
|
||||||
|
track.path = new_path
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
except IntegrityError:
|
||||||
|
# https://jira.mariadb.org/browse/MDEV-29345 workaround
|
||||||
|
session.rollback()
|
||||||
|
track.path = "DUMMY"
|
||||||
|
session.commit()
|
||||||
|
track.path = new_path
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
print(f"os.unlink({dst}")
|
||||||
|
print(f"shutil.move({src}, {new_path}")
|
||||||
|
|
||||||
|
os.unlink(dst)
|
||||||
|
shutil.move(src, new_path)
|
||||||
|
|
||||||
|
# Update track metadata
|
||||||
|
set_track_metadata(track)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
68
poetry.lock
generated
68
poetry.lock
generated
@ -1253,6 +1253,72 @@ files = [
|
|||||||
{file = "pyfzf-0.3.1.tar.gz", hash = "sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2"},
|
{file = "pyfzf-0.3.1.tar.gz", hash = "sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygame"
|
||||||
|
version = "2.5.2"
|
||||||
|
description = "Python Game Development"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da"},
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a"},
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd"},
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718"},
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1"},
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-win32.whl", hash = "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584"},
|
||||||
|
{file = "pygame-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-win32.whl", hash = "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50"},
|
||||||
|
{file = "pygame-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-win32.whl", hash = "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592"},
|
||||||
|
{file = "pygame-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4"},
|
||||||
|
{file = "pygame-2.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1"},
|
||||||
|
{file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3"},
|
||||||
|
{file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6"},
|
||||||
|
{file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945"},
|
||||||
|
{file = "pygame-2.5.2-cp36-cp36m-win32.whl", hash = "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9"},
|
||||||
|
{file = "pygame-2.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92"},
|
||||||
|
{file = "pygame-2.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae"},
|
||||||
|
{file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308"},
|
||||||
|
{file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097"},
|
||||||
|
{file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937"},
|
||||||
|
{file = "pygame-2.5.2-cp37-cp37m-win32.whl", hash = "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7"},
|
||||||
|
{file = "pygame-2.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-win32.whl", hash = "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff"},
|
||||||
|
{file = "pygame-2.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-win32.whl", hash = "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d"},
|
||||||
|
{file = "pygame-2.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6"},
|
||||||
|
{file = "pygame-2.5.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677"},
|
||||||
|
{file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4"},
|
||||||
|
{file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe"},
|
||||||
|
{file = "pygame-2.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c"},
|
||||||
|
{file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90"},
|
||||||
|
{file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9"},
|
||||||
|
{file = "pygame-2.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae"},
|
||||||
|
{file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63"},
|
||||||
|
{file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6"},
|
||||||
|
{file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.17.2"
|
version = "2.17.2"
|
||||||
@ -1970,4 +2036,4 @@ test = ["websockets"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "3b2e747f93972b78a9a35454810c99c4ec81e14fc9780e65a6a4434a97d1a713"
|
content-hash = "b33fb0a465d9bea7ec2bf14800405452d2af35e377d4c02022e33d318d8f190c"
|
||||||
|
|||||||
@ -21,6 +21,7 @@ pydymenu = "^0.5.2"
|
|||||||
stackprinter = "^0.2.10"
|
stackprinter = "^0.2.10"
|
||||||
pyqt6 = "^6.7.0"
|
pyqt6 = "^6.7.0"
|
||||||
pyqt6-webengine = "^6.7.0"
|
pyqt6-webengine = "^6.7.0"
|
||||||
|
pygame = "^2.5.2"
|
||||||
pyqtgraph = "^0.13.3"
|
pyqtgraph = "^0.13.3"
|
||||||
colorlog = "^6.8.2"
|
colorlog = "^6.8.2"
|
||||||
alchemical = "^1.0.2"
|
alchemical = "^1.0.2"
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
# Standard library imports
|
|
||||||
import os
|
|
||||||
|
|
||||||
# PyQt imports
|
|
||||||
|
|
||||||
# Third party imports
|
|
||||||
|
|
||||||
# App imports
|
|
||||||
# Set up test database before importing db
|
|
||||||
# https://blog.miguelgrinberg.com/post/how-to-write-unit-tests-in-python-part-3-web-applications
|
|
||||||
DB_FILE = "/tmp/mm.db"
|
|
||||||
if os.path.exists(DB_FILE):
|
|
||||||
os.unlink(DB_FILE)
|
|
||||||
os.environ["DATABASE_URL"] = "sqlite:///" + DB_FILE
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
@ -96,9 +95,7 @@ class TestMMHelpers(unittest.TestCase):
|
|||||||
_, mp3_temp_path = tempfile.mkstemp(suffix=".mp3")
|
_, mp3_temp_path = tempfile.mkstemp(suffix=".mp3")
|
||||||
shutil.copyfile("testdata/isa.mp3", mp3_temp_path)
|
shutil.copyfile("testdata/isa.mp3", mp3_temp_path)
|
||||||
normalise_track(mp3_temp_path)
|
normalise_track(mp3_temp_path)
|
||||||
os.unlink(mp3_temp_path)
|
|
||||||
|
|
||||||
_, flac_temp_path = tempfile.mkstemp(suffix=".flac")
|
_, flac_temp_path = tempfile.mkstemp(suffix=".flac")
|
||||||
shutil.copyfile("testdata/isa.flac", flac_temp_path)
|
shutil.copyfile("testdata/isa.flac", flac_temp_path)
|
||||||
normalise_track(flac_temp_path)
|
normalise_track(flac_temp_path)
|
||||||
os.unlink(flac_temp_path)
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
@ -7,7 +8,15 @@ import unittest
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from app.models import db, Settings
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from models import db, Settings # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class TestMMMisc(unittest.TestCase):
|
class TestMMMisc(unittest.TestCase):
|
||||||
@ -32,9 +41,10 @@ class TestMMMisc(unittest.TestCase):
|
|||||||
setting = Settings(session, SETTING_NAME)
|
setting = Settings(session, SETTING_NAME)
|
||||||
# test repr
|
# test repr
|
||||||
_ = str(setting)
|
_ = str(setting)
|
||||||
setting.f_int = VALUE
|
setting.update(session, dict(f_int=VALUE))
|
||||||
test = Settings.get_setting(session, SETTING_NAME)
|
_ = Settings.all_as_dict(session)
|
||||||
|
test = Settings.get_int_settings(session, SETTING_NAME)
|
||||||
assert test.name == SETTING_NAME
|
assert test.name == SETTING_NAME
|
||||||
assert test.f_int == VALUE
|
assert test.f_int == VALUE
|
||||||
test_new = Settings.get_setting(session, NO_SUCH_SETTING)
|
test_new = Settings.get_int_settings(session, NO_SUCH_SETTING)
|
||||||
assert test_new.name == NO_SUCH_SETTING
|
assert test_new.name == NO_SUCH_SETTING
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
@ -9,7 +10,13 @@ import unittest
|
|||||||
# App imports
|
# App imports
|
||||||
from app import helpers
|
from app import helpers
|
||||||
|
|
||||||
from app.models import (
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from app.models import ( # noqa: E402
|
||||||
db,
|
db,
|
||||||
NoteColours,
|
NoteColours,
|
||||||
Playdates,
|
Playdates,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
@ -8,8 +9,15 @@ from PyQt6.QtCore import Qt, QModelIndex
|
|||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from app.helpers import get_all_track_metadata
|
from app.helpers import get_all_track_metadata
|
||||||
from app import playlistmodel
|
|
||||||
from app.models import (
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from app import playlistmodel # noqa: E402
|
||||||
|
from app.models import ( # noqa: E402
|
||||||
db,
|
db,
|
||||||
Playlists,
|
Playlists,
|
||||||
Tracks,
|
Tracks,
|
||||||
|
|||||||
@ -3,19 +3,22 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
from PyQt6.QtCore import Qt
|
|
||||||
from PyQt6.QtGui import QColor
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
import pytest
|
import pytest
|
||||||
from pytestqt.plugin import QtBot # type: ignore
|
from pytestqt.plugin import QtBot # type: ignore
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from config import Config
|
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
from app import playlistmodel, utilities
|
from app import playlistmodel, utilities
|
||||||
from app.models import (
|
from app.models import ( # noqa: E402
|
||||||
db,
|
db,
|
||||||
NoteColours,
|
|
||||||
Playlists,
|
Playlists,
|
||||||
Tracks,
|
Tracks,
|
||||||
)
|
)
|
||||||
@ -113,9 +116,7 @@ class MyTestCase(unittest.TestCase):
|
|||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
model = playlistmodel.PlaylistModel(playlist.id)
|
||||||
|
|
||||||
# Add a track with a note
|
# Add a track with a note
|
||||||
model.insert_row(
|
model.insert_row(proposed_row_number=0, track_id=self.tracks[1]['id'], note=note_text)
|
||||||
proposed_row_number=0, track_id=self.tracks[1]["id"], note=note_text
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need to commit the session before re-querying
|
# We need to commit the session before re-querying
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -126,7 +127,7 @@ class MyTestCase(unittest.TestCase):
|
|||||||
retrieved_playlist = all_playlists[0]
|
retrieved_playlist = all_playlists[0]
|
||||||
assert len(retrieved_playlist.rows) == 1
|
assert len(retrieved_playlist.rows) == 1
|
||||||
paths = [a.track.path for a in retrieved_playlist.rows]
|
paths = [a.track.path for a in retrieved_playlist.rows]
|
||||||
assert self.tracks[1]["path"] in paths
|
assert self.tracks[1]['path'] in paths
|
||||||
notes = [a.note for a in retrieved_playlist.rows]
|
notes = [a.note for a in retrieved_playlist.rows]
|
||||||
assert note_text in notes
|
assert note_text in notes
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user