Compare commits

..

6 Commits

Author SHA1 Message Date
Keith Edmunds
2f9fcae05f All tests pass 2025-08-19 18:24:09 +01:00
Keith Edmunds
4978dcf5c3 Squash no db objects commits 2025-08-18 20:02:52 +01:00
Keith Edmunds
19b1bf3fde Fix type hint error 2025-08-17 18:42:01 +01:00
Keith Edmunds
316b4708c6 Check import filetype; install black 2025-08-17 18:26:53 +01:00
Keith Edmunds
4fd9a0381f Hide tracks, not sections 2025-08-16 15:10:45 +01:00
Keith Edmunds
88cce738d7 Move AudacityController management from playlists to musicmuster
Fixes: #292
2025-08-16 15:10:15 +01:00
7 changed files with 371 additions and 309 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_SECTIONS HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_TRACKS
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,6 +35,7 @@ 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,
@ -196,6 +197,7 @@ 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
@ -231,6 +233,27 @@ 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
@ -441,7 +464,8 @@ 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 = (
f"New import requested but default destination path ({tfd.destination_path})" "New import requested but default 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)
@ -488,7 +512,8 @@ 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 because {tfd.error}" f"{os.path.basename(tfd.source_path)} will not be imported "
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))
@ -511,7 +536,8 @@ 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(
f"remaining files: {[a.source_path for a in self.import_files_data]}" "remaining files: "
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
@ -608,7 +634,10 @@ class DoTrackImport(QThread):
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<DoTrackImport(id={hex(id(self))}, import_file_path={self.import_file_path}" return (
f"<DoTrackImport(id={hex(id(self))}, "
f"import_file_path={self.import_file_path}"
)
def run(self) -> None: def run(self) -> None:
""" """
@ -622,7 +651,8 @@ 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 function to save interactive time # Get audio metadata in this thread rather than calling
# 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,6 +13,7 @@ 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
@ -49,6 +50,14 @@ 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,
@ -93,6 +102,9 @@ 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,6 +63,7 @@ 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,
@ -79,6 +80,7 @@ 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
@ -1232,6 +1234,13 @@ 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,7 +34,6 @@ 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,
@ -315,13 +314,6 @@ 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()
@ -554,8 +546,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: if track_row and not this_is_current_row and self.musicmuster.ac:
if self.ac and track_path == self.ac.path: if track_path == self.musicmuster.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",
@ -665,8 +657,8 @@ class PlaylistTab(QTableView):
that we have an edit open. that we have an edit open.
""" """
if self.ac: if self.musicmuster.ac:
self.ac.path = None self.musicmuster.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"""
@ -877,10 +869,10 @@ class PlaylistTab(QTableView):
Import current Audacity track to passed row Import current Audacity track to passed row
""" """
if not self.ac: if not self.musicmuster.ac:
return return
try: try:
self.ac.export() self.ac.musicmuster.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))
@ -937,15 +929,16 @@ 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:
if not self.ac: self.musicmuster.ac.open(path)
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,6 +33,8 @@ 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]
@ -63,6 +65,9 @@ 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