Compare commits

..

86 Commits

Author SHA1 Message Date
Keith Edmunds
206a02214e All tests pass 2025-08-18 13:11:35 +01:00
Keith Edmunds
7918bbb237 Make FadegraphThreadController a singleton 2025-04-25 17:51:21 +01:00
Keith Edmunds
8621a37af3 Remove spurious line 2025-04-25 17:50:54 +01:00
Keith Edmunds
ad391aedc6 Rewrite thread management for fade graph generation 2025-04-24 11:49:04 +01:00
Keith Edmunds
735c927864 Make Signals more apparent in logs (prefix SIGNAL) 2025-04-23 17:12:02 +01:00
Keith Edmunds
e2aac65dac Remove spurious '.id' references 2025-04-23 17:11:33 +01:00
Keith Edmunds
86c3c3fd80 Black 2025-04-22 21:56:44 +01:00
Keith Edmunds
a4ba013306 Change id columns to TABLENAME_id 2025-04-22 20:18:56 +01:00
Keith Edmunds
6d012d7b5a Remove app/models.py
dbtables.py now contains table definitions, __repr__ and __init__
2025-04-21 22:30:16 +01:00
Keith Edmunds
c58eb47cc1 musicmuster.current cleanup 2025-04-21 11:49:10 +01:00
Keith Edmunds
847840251c More signal / signal handler cleanups 2025-04-21 11:48:16 +01:00
Keith Edmunds
f172eaaeb2 Clean up signals
Remove search_wikipedia_signal and search_songfacts_signal. Improve
signal names. Make Current a dataclass.
2025-04-21 10:37:27 +01:00
Keith Edmunds
25771f5235 Slightly simplify track times; appears to be working 2025-04-20 22:08:25 +01:00
Keith Edmunds
c93d24aef5 Row timings improved but next row not always correct 2025-04-20 21:37:18 +01:00
Keith Edmunds
a2a9afb04f Rewrite track timings; looks to be working 2025-04-19 21:45:53 +01:00
Keith Edmunds
db6fb7b367 Cleanup signals 2025-04-19 21:45:26 +01:00
Keith Edmunds
5d7b55a0ab Change track_ended_signal to signal_track_ended 2025-04-19 18:25:56 +01:00
Keith Edmunds
0124ca6018 Cleanup 2025-04-19 18:23:43 +01:00
Keith Edmunds
af40e419ff Fix up tests 2025-04-19 12:27:06 +01:00
Keith Edmunds
223c7cd3ab Use vlc events to trigger end-of-track actions 2025-04-19 12:26:44 +01:00
Keith Edmunds
edd8c36c53 Use signals for setting next track 2025-04-19 12:25:29 +01:00
Keith Edmunds
c6be215bd4 Log signals; show debug to console 2025-04-17 19:13:13 +01:00
Keith Edmunds
22e16144e1 Remove more vlc loggins 2025-04-16 21:03:45 +01:00
Keith Edmunds
1932719fea WIP using signals and no sessions 2025-04-16 21:03:02 +01:00
Keith Edmunds
e9f4ecf5ef Remove vlc logging 2025-04-16 21:02:33 +01:00
Keith Edmunds
ce224a41d1 Implement signal monitor 2025-04-16 21:02:21 +01:00
Keith Edmunds
bf89172f8a Move definition of db into dbmanager 2025-04-16 21:01:35 +01:00
Keith Edmunds
9f234bb007 Fix segfault inserting tracks 2025-04-16 10:33:01 +01:00
Keith Edmunds
6fb6339843 Much improved row move implementation 2025-04-16 10:23:44 +01:00
Keith Edmunds
bf5563f2f1 Black changes 2025-04-16 10:23:17 +01:00
Keith Edmunds
8fa98a2207 Have model check for adding existing track 2025-04-15 21:22:07 +01:00
Keith Edmunds
e7af25ad6e Restore last open playlists in correct order 2025-04-15 18:24:56 +01:00
Keith Edmunds
d6bb3d04d8 Row edit updates now handled in PlaylistRow 2025-04-15 10:22:10 +01:00
Keith Edmunds
a0ded4b73d Handle signal_insert_track better. 2025-04-15 10:21:26 +01:00
Keith Edmunds
6496ea2ac4 Don't close when track playing; mark as played in playlist 2025-04-14 21:23:00 +01:00
Keith Edmunds
c61df17dd5 Mark playlist rows played in db 2025-04-14 20:09:14 +01:00
Keith Edmunds
747f28f4f9 WIP: track ending signal 2025-04-14 19:18:26 +01:00
Keith Edmunds
5f0da55a24 Remove old ui files 2025-04-14 12:52:59 +01:00
Keith Edmunds
498923c3b3 Connect up signal_insert_track 2025-04-14 09:45:06 +01:00
Keith Edmunds
b34e0a014a WIP: more database updates; all tests run 2025-04-13 19:49:05 +01:00
Keith Edmunds
f9c33120f5 Document and clean up signals 2025-04-13 18:05:00 +01:00
Keith Edmunds
ffb1b238f4 WIP: Make ds.track_update more generic 2025-04-13 16:34:37 +01:00
Keith Edmunds
83780bfb68 Clean up unused signals 2025-04-13 15:38:09 +01:00
Keith Edmunds
cd793f9668 WIP: fix ds names, tidy row moving 2025-04-13 15:17:01 +01:00
Keith Edmunds
7f3e235e9d All tests pass 2025-04-13 13:13:49 +01:00
Keith Edmunds
324dd770df test_ui tests pass 2025-04-13 12:59:26 +01:00
Keith Edmunds
11400536b5 test_file_importer tests pass 2025-04-13 12:22:41 +01:00
Keith Edmunds
d596792375 All test_queries tests pass 2025-04-13 10:29:54 +01:00
Keith Edmunds
a8791f925d All test_playlistmodel tests pass 2025-04-13 10:17:58 +01:00
Keith Edmunds
5317ecdf18 Remove sessions from test_misc.py 2025-04-13 09:21:36 +01:00
Keith Edmunds
a2baf489c3 Remove sessions from test_playlistmodel.py 2025-04-13 09:16:49 +01:00
Keith Edmunds
aec994bafd Rename ds functions; fix add track to header 2025-04-13 09:12:33 +01:00
Keith Edmunds
0c717241ff Command out @log_call decorators 2025-04-12 19:24:25 +01:00
Keith Edmunds
728d012257 WIP: working on tests 2025-04-12 19:16:09 +01:00
Keith Edmunds
ac685426d9 Implement NoteColoursDTO 2025-04-12 11:15:54 +01:00
Keith Edmunds
38c49b32d7 WIP: rename repository.py → ds.py 2025-04-12 11:15:21 +01:00
Keith Edmunds
9a6bb038e1 WIP: fix insert tracks from query at selected row 2025-04-12 10:31:11 +01:00
Keith Edmunds
0478e25109 Fix bug inserting multiple tracks from query 2025-04-12 09:28:22 +01:00
Keith Edmunds
199abc9c0c Fix showing tracks from queries 2025-04-11 17:13:03 +01:00
Keith Edmunds
f9c8541b17 WIP: moving to signals 2025-04-08 16:11:39 +01:00
Keith Edmunds
5e492f4569 WIP: sessions only in repository and mocels 2025-04-07 18:33:16 +01:00
Keith Edmunds
f3b1e05e83 WIP Remove repository.py dependency on helpers.py 2025-04-05 16:32:31 +01:00
Keith Edmunds
e71f1d072f Update environment 2025-04-05 15:52:34 +01:00
Keith Edmunds
c182a69a5d WIP: remove session from playlistmodel 2025-04-04 18:54:46 +01:00
Keith Edmunds
e39518e5ee WIP: remove sessions from file_importer 2025-04-04 18:52:31 +01:00
Keith Edmunds
4eaab98745 WIP: progressing no sessions in app files 2025-04-04 16:19:17 +01:00
Keith Edmunds
2fce0b34be WIP: clean up imports 2025-04-04 16:17:51 +01:00
Keith Edmunds
ed7ac0758c No more sessions in playlists!
Save/restore playlist column widths.
2025-03-30 13:55:17 +01:00
Keith Edmunds
098ce7198e Remove stack dump from ApplicationError dialog 2025-03-30 13:54:10 +01:00
Keith Edmunds
38b166737b WIP: add track to header
Logic works; playlistrow.py doesn't yet update database but prints a
message saying db needs to be updated.
2025-03-30 13:30:04 +01:00
Keith Edmunds
0ea12eb9d9 Clean up merge from dev 2025-03-30 12:05:41 +01:00
Keith Edmunds
75cc7a3f19 Merge changes from dev 2025-03-30 11:56:24 +01:00
Keith Edmunds
f64671d126 Improve function logging
Use @log_call decorator

Add 'checked' parameter to menu slots because PyQt6 will pass a
boolean 'checked' parameter even when the menu item can't be checked.

Remove superfluous logging calls.
2025-03-29 21:04:59 +00:00
Keith Edmunds
2bf1bc64e7 WIP: Unify function to move rows within/between playlists 2025-03-29 18:20:38 +00:00
Keith Edmunds
3c7fc20e5a Remove kae.py from git 2025-03-29 18:20:38 +00:00
Keith Edmunds
52d2269ece WIP: Move rows within and between playlists working 2025-03-29 18:20:38 +00:00
Keith Edmunds
3cd764c893 WIP: moving rows within playlist works 2025-03-29 18:20:38 +00:00
Keith Edmunds
65878b0b75 WIP: all tests for move rows within playlist working 2025-03-29 18:20:38 +00:00
Keith Edmunds
4e89d72a8f WIP: move within playlist tests working 2025-03-29 18:20:38 +00:00
Keith Edmunds
92ecb632b5 Report correct line for ApplicationError 2025-03-29 18:20:38 +00:00
Keith Edmunds
6296566c2c WIP: Can play tracks without errors 2025-03-29 18:20:38 +00:00
Keith Edmunds
7e5b170f5e Use @singleton decorator 2025-03-29 18:20:38 +00:00
Keith Edmunds
3db71a08ae WIP remove sessions, use reporistory 2025-03-29 18:20:38 +00:00
Keith Edmunds
7b0e2b2c6c WIP: playlists load, can't play track 2025-03-29 18:20:38 +00:00
Keith Edmunds
4265472d73 Keep track of selected rows in model 2025-03-29 18:20:38 +00:00
Keith Edmunds
c94cadf24f WIP: Use PlaylistRowDTO to isolate SQLAlchemy objects 2025-03-29 18:20:38 +00:00
7 changed files with 309 additions and 371 deletions

View File

@ -143,6 +143,6 @@ class Config(object):
WIKIPEDIA_ON_NEXT = False WIKIPEDIA_ON_NEXT = False
# These rely on earlier definitions # These rely on earlier definitions
HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_TRACKS HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_SECTIONS
IMPORT_DESTINATION = os.path.join(ROOT, "Singles") IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE) REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE)

View File

@ -35,7 +35,6 @@ from classes import (
) )
from config import Config from config import Config
from helpers import ( from helpers import (
audio_file_extension,
file_is_unreadable, file_is_unreadable,
get_all_track_metadata, get_all_track_metadata,
get_audio_metadata, get_audio_metadata,
@ -197,9 +196,8 @@ class FileImporter:
self.sort_track_match_data(tfd) self.sort_track_match_data(tfd)
selection = self.get_user_choices(tfd) selection = self.get_user_choices(tfd)
if self.process_selection(tfd, selection): if self.process_selection(tfd, selection):
if self.extension_check(tfd): if self.validate_file_data(tfd):
if self.validate_file_data(tfd): tfd.import_this_file = True
tfd.import_this_file = True
return tfd return tfd
@ -233,27 +231,6 @@ class FileImporter:
return True return True
def extension_check(self, tfd: TrackFileData) -> bool:
"""
If we are replacing an existing file, check that the correct file
extension of the replacement file matches the existing file
extension and return True if it does (or if there is no exsting
file), else False.
"""
if not tfd.file_path_to_remove:
return True
extension = audio_file_extension(tfd.source_path)
if extension and tfd.file_path_to_remove.endswith(extension):
return True
tfd.error = (
f"Existing file ({tfd.file_path_to_remove}) has a different "
f"extension to replacement file ({tfd.source_path})"
)
return False
def find_similar(self, tfd: TrackFileData) -> None: def find_similar(self, tfd: TrackFileData) -> None:
""" """
- Search title in existing tracks - Search title in existing tracks
@ -464,8 +441,7 @@ class FileImporter:
if tfd.track_id == 0 and tfd.destination_path != tfd.file_path_to_remove: if tfd.track_id == 0 and tfd.destination_path != tfd.file_path_to_remove:
while os.path.exists(tfd.destination_path): while os.path.exists(tfd.destination_path):
msg = ( msg = (
"New import requested but default destination path" f"New import requested but default destination path ({tfd.destination_path})"
f" ({tfd.destination_path})"
" already exists. Click OK and choose where to save this track" " already exists. Click OK and choose where to save this track"
) )
show_OK(title="Desintation path exists", msg=msg, parent=None) show_OK(title="Desintation path exists", msg=msg, parent=None)
@ -512,8 +488,7 @@ class FileImporter:
msgs: list[str] = [] msgs: list[str] = []
for tfd in tfds: for tfd in tfds:
msgs.append( msgs.append(
f"{os.path.basename(tfd.source_path)} will not be imported " f"{os.path.basename(tfd.source_path)} will not be imported because {tfd.error}"
f"because {tfd.error}"
) )
if msgs: if msgs:
show_OK("File not imported", "\r\r".join(msgs)) show_OK("File not imported", "\r\r".join(msgs))
@ -536,8 +511,7 @@ class FileImporter:
filename = os.path.basename(tfd.source_path) filename = os.path.basename(tfd.source_path)
log.debug(f"Processing {filename}") log.debug(f"Processing {filename}")
log.debug( log.debug(
"remaining files: " f"remaining files: {[a.source_path for a in self.import_files_data]}"
f"{[a.source_path for a in self.import_files_data]}"
) )
self.signals.status_message_signal.emit( self.signals.status_message_signal.emit(
f"Importing {filename}", 10000 f"Importing {filename}", 10000
@ -634,10 +608,7 @@ class DoTrackImport(QThread):
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return f"<DoTrackImport(id={hex(id(self))}, import_file_path={self.import_file_path}"
f"<DoTrackImport(id={hex(id(self))}, "
f"import_file_path={self.import_file_path}"
)
def run(self) -> None: def run(self) -> None:
""" """
@ -651,8 +622,7 @@ class DoTrackImport(QThread):
f"Importing {os.path.basename(self.import_file_path)}", 5000 f"Importing {os.path.basename(self.import_file_path)}", 5000
) )
# Get audio metadata in this thread rather than calling # Get audio metadata in this thread rather than calling function to save interactive time
# function to save interactive time
self.audio_metadata = get_audio_metadata(self.import_file_path) self.audio_metadata = get_audio_metadata(self.import_file_path)
# Remove old file if so requested # Remove old file if so requested

View File

@ -13,7 +13,6 @@ import tempfile
from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget
# Third party imports # Third party imports
import filetype
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
@ -50,14 +49,6 @@ def ask_yes_no(
return button == QMessageBox.StandardButton.Yes return button == QMessageBox.StandardButton.Yes
def audio_file_extension(fpath: str) -> str | None:
"""
Return the correct extension for this type of file.
"""
return filetype.guess(fpath).extension
def fade_point( def fade_point(
audio_segment: AudioSegment, audio_segment: AudioSegment,
fade_threshold: float = 0.0, fade_threshold: float = 0.0,
@ -80,7 +71,7 @@ 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 and trim_ms > 0
): # noqa W503 ): # noqa W503
trim_ms -= chunk_size trim_ms -= chunk_size
@ -102,9 +93,6 @@ def file_is_unreadable(path: Optional[str]) -> bool:
def get_audio_segment(path: str) -> Optional[AudioSegment]: def get_audio_segment(path: str) -> Optional[AudioSegment]:
if not path.endswith(audio_file_extension(path)):
return None
try: try:
if path.endswith(".mp3"): if path.endswith(".mp3"):
return AudioSegment.from_mp3(path) return AudioSegment.from_mp3(path)

View File

@ -63,7 +63,6 @@ from pygame import mixer
import stackprinter # type: ignore import stackprinter # type: ignore
# App imports # App imports
from audacity_controller import AudacityController
from classes import ( from classes import (
ApplicationError, ApplicationError,
Filter, Filter,
@ -80,7 +79,6 @@ from dialogs import TrackInsertDialog
from file_importer import FileImporter from file_importer import FileImporter
from helpers import file_is_unreadable, get_name from helpers import file_is_unreadable, get_name
from log import log, log_call from log import log, log_call
from helpers import ask_yes_no, file_is_unreadable, get_name, show_warning
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
from playlistrow import PlaylistRow, TrackSequence from playlistrow import PlaylistRow, TrackSequence
from playlists import PlaylistTab from playlists import PlaylistTab
@ -1234,13 +1232,6 @@ class Window(QMainWindow):
# Load playlists # Load playlists
self.load_last_playlists() self.load_last_playlists()
# Set up for Audacity
try:
self.ac: Optional[AudacityController] = AudacityController()
except ApplicationError as e:
self.ac = None
show_warning(self, "Audacity error", str(e))
# # # # # # # # # # Overrides # # # # # # # # # # # # # # # # # # # # Overrides # # # # # # # # # #
def closeEvent(self, event: QCloseEvent | None) -> None: def closeEvent(self, event: QCloseEvent | None) -> None:

View File

@ -34,6 +34,7 @@ from PyQt6.QtWidgets import (
# import line_profiler # import line_profiler
# App imports # App imports
from audacity_controller import AudacityController
from classes import ( from classes import (
ApplicationError, ApplicationError,
Col, Col,
@ -314,6 +315,13 @@ class PlaylistTab(QTableView):
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# Set up for Audacity
try:
self.ac: Optional[AudacityController] = AudacityController()
except ApplicationError as e:
self.ac = None
show_warning(self.musicmuster, "Audacity error", str(e))
# Load model, set column widths # Load model, set column widths
self.setModel(model) self.setModel(model)
self._set_column_widths() self._set_column_widths()
@ -546,8 +554,8 @@ class PlaylistTab(QTableView):
track_path = base_model.get_row_info(model_row_number).path track_path = base_model.get_row_info(model_row_number).path
# Open/import in/from Audacity # Open/import in/from Audacity
if track_row and not this_is_current_row and self.musicmuster.ac: if track_row and not this_is_current_row:
if track_path == self.musicmuster.ac.path: if self.ac and track_path == self.ac.path:
# This track was opened in Audacity # This track was opened in Audacity
self._add_context_menu( self._add_context_menu(
"Update from Audacity", "Update from Audacity",
@ -657,8 +665,8 @@ class PlaylistTab(QTableView):
that we have an edit open. that we have an edit open.
""" """
if self.musicmuster.ac: if self.ac:
self.musicmuster.ac.path = None self.ac.path = None
def clear_selection(self) -> None: def clear_selection(self) -> None:
"""Unselect all tracks and reset drag mode""" """Unselect all tracks and reset drag mode"""
@ -869,10 +877,10 @@ class PlaylistTab(QTableView):
Import current Audacity track to passed row Import current Audacity track to passed row
""" """
if not self.musicmuster.ac: if not self.ac:
return return
try: try:
self.ac.musicmuster.export() self.ac.export()
self._rescan(row_number) self._rescan(row_number)
except ApplicationError as e: except ApplicationError as e:
show_warning(self.musicmuster, "Audacity error", str(e)) show_warning(self.musicmuster, "Audacity error", str(e))
@ -929,16 +937,15 @@ class PlaylistTab(QTableView):
Open track in passed row in Audacity Open track in passed row in Audacity
""" """
if not self.musicmuster.ac:
return
path = self.get_base_model().get_row_track_path(row_number) path = self.get_base_model().get_row_track_path(row_number)
if not path: if not path:
log.error(f"_open_in_audacity: can't get path for {row_number=}") log.error(f"_open_in_audacity: can't get path for {row_number=}")
return return
try: try:
self.musicmuster.ac.open(path) if not self.ac:
self.ac = AudacityController()
self.ac.open(path)
except ApplicationError as e: except ApplicationError as e:
show_warning(self.musicmuster, "Audacity error", str(e)) show_warning(self.musicmuster, "Audacity error", str(e))

View File

@ -33,8 +33,6 @@ dependencies = [
"types-pyyaml>=6.0.12.20241230", "types-pyyaml>=6.0.12.20241230",
"dogpile-cache>=1.3.4", "dogpile-cache>=1.3.4",
"pdbpp>=0.10.3", "pdbpp>=0.10.3",
"filetype>=1.2.0",
"black>=25.1.0",
] ]
[dependency-groups] [dependency-groups]
@ -65,9 +63,6 @@ python_version = 3.11
warn_unused_configs = true warn_unused_configs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
[tool.pylsp.plugins.pycodestyle]
maxLineLength = 88
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--exitfirst --showlocals --capture=no" addopts = "--exitfirst --showlocals --capture=no"
pythonpath = [".", "app"] pythonpath = [".", "app"]

579
uv.lock

File diff suppressed because it is too large Load Diff