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
# 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")
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 helpers import (
audio_file_extension,
file_is_unreadable,
get_all_track_metadata,
get_audio_metadata,
@ -196,8 +197,9 @@ class FileImporter:
self.sort_track_match_data(tfd)
selection = self.get_user_choices(tfd)
if self.process_selection(tfd, selection):
if self.validate_file_data(tfd):
tfd.import_this_file = True
if self.extension_check(tfd):
if self.validate_file_data(tfd):
tfd.import_this_file = True
return tfd
@ -231,6 +233,27 @@ class FileImporter:
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:
"""
- 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:
while os.path.exists(tfd.destination_path):
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"
)
show_OK(title="Desintation path exists", msg=msg, parent=None)
@ -488,7 +512,8 @@ class FileImporter:
msgs: list[str] = []
for tfd in tfds:
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:
show_OK("File not imported", "\r\r".join(msgs))
@ -511,7 +536,8 @@ class FileImporter:
filename = os.path.basename(tfd.source_path)
log.debug(f"Processing {filename}")
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(
f"Importing {filename}", 10000
@ -608,7 +634,10 @@ class DoTrackImport(QThread):
self.signals = MusicMusterSignals()
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:
"""
@ -622,7 +651,8 @@ class DoTrackImport(QThread):
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)
# Remove old file if so requested

View File

@ -13,6 +13,7 @@ import tempfile
from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget
# Third party imports
import filetype
from mutagen.flac import FLAC # type: ignore
from mutagen.mp3 import MP3 # type: ignore
from pydub import AudioSegment, effects
@ -49,6 +50,14 @@ def ask_yes_no(
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(
audio_segment: AudioSegment,
fade_threshold: float = 0.0,
@ -71,7 +80,7 @@ def fade_point(
fade_threshold = max_vol
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
trim_ms -= chunk_size
@ -93,6 +102,9 @@ def file_is_unreadable(path: Optional[str]) -> bool:
def get_audio_segment(path: str) -> Optional[AudioSegment]:
if not path.endswith(audio_file_extension(path)):
return None
try:
if path.endswith(".mp3"):
return AudioSegment.from_mp3(path)

View File

@ -63,6 +63,7 @@ from pygame import mixer
import stackprinter # type: ignore
# App imports
from audacity_controller import AudacityController
from classes import (
ApplicationError,
Filter,
@ -79,6 +80,7 @@ from dialogs import TrackInsertDialog
from file_importer import FileImporter
from helpers import file_is_unreadable, get_name
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 playlistrow import PlaylistRow, TrackSequence
from playlists import PlaylistTab
@ -1232,6 +1234,13 @@ class Window(QMainWindow):
# Load 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 # # # # # # # # # #
def closeEvent(self, event: QCloseEvent | None) -> None:

View File

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

View File

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

579
uv.lock

File diff suppressed because it is too large Load Diff