Compare commits

...

16 Commits

Author SHA1 Message Date
Keith Edmunds
8956642e05 Remove unused import. 2026-01-04 13:58:57 +00:00
Keith Edmunds
791fad680a Clean up old error logging; log excessive jitter. 2026-01-04 13:54:14 +00:00
Keith Edmunds
8c60d6a03d Jitter monitor phase 0 2026-01-03 21:37:50 +00:00
Keith Edmunds
7391b4e61c Start header in column zero 2026-01-03 18:44:36 +00:00
Keith Edmunds
266be281d0 Don't run update_track_times twice on starting track 2026-01-02 14:34:28 +00:00
Keith Edmunds
ac487a5fa5 Make vlc volume default 100 2026-01-01 17:18:58 +00:00
Keith Edmunds
7d1bb0d3f7 Escape double quotes in filename 2025-12-14 15:58:35 +00:00
Keith Edmunds
e8d9cf8f00 Ensure track marked as played in playlist
Fixes #295
2025-09-26 00:38:44 +01:00
Keith Edmunds
7e7ae7dddf Tidy config 2025-09-25 15:07:30 +01:00
Keith Edmunds
25cb444335 Set default volumen to 80% 2025-09-25 14:53:30 +01:00
Keith Edmunds
fa14fc7c52 Fixup reloading track from Audacity 2025-08-23 12:18:06 +01:00
Keith Edmunds
6e51e65ba8 Add .gitattibutes to define python diffing 2025-08-23 12:17:42 +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
13 changed files with 577 additions and 491 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.py diff=python

View File

@ -69,7 +69,8 @@ class AudacityController:
select_status = self._send_command("SelectAll") select_status = self._send_command("SelectAll")
log.debug(f"{select_status=}") log.debug(f"{select_status=}")
export_cmd = f'Export2: Filename="{self.path}" NumChannels=2' # Escape any double quotes in filename
export_cmd = f'Export2: Filename="{self.path.replace('"', '\\"')}" NumChannels=2'
export_status = self._send_command(export_cmd) export_status = self._send_command(export_cmd)
log.debug(f"{export_status=}") log.debug(f"{export_status=}")
self.path = "" self.path = ""

View File

@ -132,14 +132,14 @@ class Config(object):
TRACK_TIME_FORMAT = "%H:%M:%S" TRACK_TIME_FORMAT = "%H:%M:%S"
VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player" VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"
VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player" VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player"
VLC_VOLUME_DEFAULT = 75 VLC_VOLUME_DEFAULT = 100
VLC_VOLUME_DROP3db = 65 VLC_VOLUME_DROP3db = 70
WARNING_MS_BEFORE_FADE = 5500 WARNING_MS_BEFORE_FADE = 5500
WARNING_MS_BEFORE_SILENCE = 5500 WARNING_MS_BEFORE_SILENCE = 5500
WEB_ZOOM_FACTOR = 1.2 WEB_ZOOM_FACTOR = 1.2
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_tags, get_tags,
show_OK, show_OK,
@ -104,9 +105,7 @@ class FileImporter:
# variable or an instance variable are effectively the same thing. # variable or an instance variable are effectively the same thing.
workers: dict[str, DoTrackImport] = {} workers: dict[str, DoTrackImport] = {}
def __init__( def __init__(self, base_model: PlaylistModel, row_number: int) -> None:
self, base_model: PlaylistModel, row_number: int = None
) -> None:
""" """
Initialise the FileImporter singleton instance. Initialise the FileImporter singleton instance.
""" """
@ -202,8 +201,9 @@ 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.validate_file_data(tfd): if self.extension_check(tfd):
tfd.import_this_file = True if self.validate_file_data(tfd):
tfd.import_this_file = True
return tfd return tfd
@ -237,6 +237,26 @@ 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
if tfd.file_path_to_remove.endswith(audio_file_extension(tfd.source_path)):
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
@ -445,7 +465,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)
@ -627,7 +648,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 = helpers.get_audio_metadata(self.import_file_path) self.audio_metadata = helpers.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
@ -50,6 +51,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,
@ -72,7 +81,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
@ -94,6 +103,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)

56
app/jittermonitor.py Normal file
View File

@ -0,0 +1,56 @@
from PyQt6.QtCore import QObject, QTimer, QElapsedTimer
import logging
import time
from config import Config
class EventLoopJitterMonitor(QObject):
def __init__(
self,
parent=None,
interval_ms: int = 20,
jitter_threshold_ms: int = 100,
log_cooldown_s: float = 1.0,
):
super().__init__(parent)
self._interval = interval_ms
self._jitter_threshold = jitter_threshold_ms
self._log_cooldown_s = log_cooldown_s
self._timer = QTimer(self)
self._timer.setInterval(self._interval)
self._timer.timeout.connect(self._on_timeout)
self._elapsed = QElapsedTimer()
self._elapsed.start()
self._last = self._elapsed.elapsed()
# child logger: e.g. "musicmuster.jitter"
self._log = logging.getLogger(f"{Config.LOG_NAME}.jitter")
self._last_log_time = 0.0
def start(self) -> None:
self._timer.start()
def _on_timeout(self) -> None:
now_ms = self._elapsed.elapsed()
delta = now_ms - self._last
self._last = now_ms
if delta > (self._interval + self._jitter_threshold):
self._log_jitter(now_ms, delta)
def _log_jitter(self, now_ms: int, gap_ms: int) -> None:
now = time.monotonic()
# simple rate limit: only one log every log_cooldown_s
if now - self._last_log_time < self._log_cooldown_s:
return
self._last_log_time = now
self._log.warning(
"Event loop gap detected: t=%d ms, gap=%d ms (interval=%d ms)",
now_ms,
gap_ms,
self._interval,
)

View File

@ -23,8 +23,8 @@ filters:
# - function-name-1 # - function-name-1
# - function-name-2 # - function-name-2
musicmuster: musicmuster:
- update_clocks
- play_next - play_next
jittermonitor: []
handlers: handlers:
stderr: stderr:

View File

@ -30,6 +30,7 @@ from log import log
from models import PlaylistRows from models import PlaylistRows
from vlcmanager import VLCManager from vlcmanager import VLCManager
# Define the VLC callback function type # Define the VLC callback function type
# import ctypes # import ctypes
# import platform # import platform
@ -353,21 +354,6 @@ class _Music:
self.player.set_position(position) self.player.set_position(position)
self.start_dt = start_time self.start_dt = start_time
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
# It has been known for the volume to need correcting more
# than once in the first 200mS.
# Update August 2024: This no longer seems to be an issue
# for _ in range(3):
# if self.player:
# volume = self.player.audio_get_volume()
# if volume < Config.VLC_VOLUME_DEFAULT:
# self.set_volume(Config.VLC_VOLUME_DEFAULT)
# log.error(f"Reset from {volume=}")
# sleep(0.1)
def set_position(self, position: float) -> None: def set_position(self, position: float) -> None:
""" """
Set player position Set player position
@ -391,17 +377,6 @@ class _Music:
volume = Config.VLC_VOLUME_DEFAULT volume = Config.VLC_VOLUME_DEFAULT
self.player.audio_set_volume(volume) self.player.audio_set_volume(volume)
# Ensure volume correct
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
for _ in range(3):
current_volume = self.player.audio_get_volume()
if current_volume < volume:
self.player.audio_set_volume(volume)
log.debug(f"Reset from {volume=}")
sleep(0.1)
def stop(self) -> None: def stop(self) -> None:
"""Immediately stop playing""" """Immediately stop playing"""

View File

@ -63,6 +63,7 @@ from sqlalchemy.orm.session import Session
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,
@ -72,8 +73,8 @@ from classes import (
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog
from file_importer import FileImporter from file_importer import FileImporter
from helpers import ask_yes_no, 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 models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks
from music_manager import RowAndTrack, track_sequence from music_manager import RowAndTrack, track_sequence
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
@ -89,6 +90,7 @@ from ui.main_window_footer_ui import Ui_FooterSection # type: ignore
from utilities import check_db, update_bitrates from utilities import check_db, update_bitrates
import helpers import helpers
from jittermonitor import EventLoopJitterMonitor
class Current: class Current:
base_model: PlaylistModel base_model: PlaylistModel
@ -1205,9 +1207,26 @@ class Window(QMainWindow):
self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self) self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self)
self.action_quicklog.activated.connect(self.quicklog) self.action_quicklog.activated.connect(self.quicklog)
# Jitter monitor - log delays in main event loop
self.jitter_monitor = EventLoopJitterMonitor(
parent=self,
interval_ms=20,
jitter_threshold_ms=100, # only care about >~100 ms
log_cooldown_s=1.0, # at most 1 warning per second
)
self.jitter_monitor.start()
self.load_last_playlists() self.load_last_playlists()
self.stop_autoplay = False self.stop_autoplay = False
# 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: Optional[QCloseEvent]) -> None: def closeEvent(self, event: Optional[QCloseEvent]) -> None:
@ -2198,14 +2217,6 @@ class Window(QMainWindow):
if self.return_pressed_in_error(): if self.return_pressed_in_error():
return return
# Issue #223 concerns a very short pause (maybe 0.1s) sometimes
# when starting to play at track. Resolution appears to be to
# disable timer10 for a short time. Timer is re-enabled in
# update_clocks.
self.timer10.stop()
log.debug("issue223: play_next: 10ms timer disabled")
# If there's currently a track playing, fade it. # If there's currently a track playing, fade it.
if track_sequence.current: if track_sequence.current:
track_sequence.current.fade() track_sequence.current.fade()
@ -2717,8 +2728,7 @@ class Window(QMainWindow):
# WARNING_MS_BEFORE_FADE milliseconds before fade starts, set # WARNING_MS_BEFORE_FADE milliseconds before fade starts, set
# warning colour on time to silence box and enable play # warning colour on time to silence box and enable play
# controls. This is also a good time to re-enable the 10ms # controls.
# timer (see play_next() and issue #223).
elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE: elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE:
self.footer_section.frame_fade.setStyleSheet( self.footer_section.frame_fade.setStyleSheet(
@ -2726,11 +2736,6 @@ class Window(QMainWindow):
) )
self.catch_return_key = False self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0) self.show_status_message("Play controls: Enabled", 0)
# Re-enable 10ms timer (see above)
log.debug(f"issue287: {self.timer10.isActive()=}")
if not self.timer10.isActive():
self.timer10.start(10)
log.debug("issue223: update_clocks: 10ms timer enabled")
else: else:
self.footer_section.frame_silent.setStyleSheet("") self.footer_section.frame_silent.setStyleSheet("")

View File

@ -51,7 +51,7 @@ from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from music_manager import RowAndTrack, track_sequence from music_manager import RowAndTrack, track_sequence
HEADER_NOTES_COLUMN = 1 HEADER_NOTES_COLUMN = 0
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
@ -288,7 +288,6 @@ class PlaylistModel(QAbstractTableModel):
# Update Playdates in database # Update Playdates in database
log.debug(f"{self}: update playdates {track_id=}") log.debug(f"{self}: update playdates {track_id=}")
Playdates(session, track_id) Playdates(session, track_id)
session.commit()
# Mark track as played in playlist # Mark track as played in playlist
log.debug(f"{self}: Mark track as played") log.debug(f"{self}: Mark track as played")
@ -298,39 +297,42 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_row(session, plr.row_number) self.refresh_row(session, plr.row_number)
else: else:
log.error( log.error(
f"{self}: Can't retrieve plr, {track_sequence.current.playlistrow_id=}" f"{self}: Can't retrieve plr, "
f"{track_sequence.current.playlistrow_id=}"
) )
session.commit()
# Update colour and times for current row # Update colour and times for current row
# only invalidate required roles
roles = [
Qt.ItemDataRole.DisplayRole
]
self.invalidate_row(row_number, roles)
# Update previous row in case we're hiding played rows
if track_sequence.previous and track_sequence.previous.row_number:
# only invalidate required roles # only invalidate required roles
roles = [ self.invalidate_row(track_sequence.previous.row_number, roles)
Qt.ItemDataRole.DisplayRole
]
self.invalidate_row(row_number, roles)
# Update previous row in case we're hiding played rows # Find next track
if track_sequence.previous and track_sequence.previous.row_number: next_row = None
# only invalidate required roles unplayed_rows = [
self.invalidate_row(track_sequence.previous.row_number, roles) a
for a in self.get_unplayed_rows()
# Update all other track times if not self.is_header_row(a)
and not file_is_unreadable(self.playlist_rows[a].path)
]
if unplayed_rows:
try:
next_row = min([a for a in unplayed_rows if a > row_number])
except ValueError:
next_row = min(unplayed_rows)
if next_row is not None:
self.set_next_row(next_row)
else:
# set_next_row() calls update_track_times(); else we call it
self.update_track_times() self.update_track_times()
# Find next track
next_row = None
unplayed_rows = [
a
for a in self.get_unplayed_rows()
if not self.is_header_row(a)
and not file_is_unreadable(self.playlist_rows[a].path)
]
if unplayed_rows:
try:
next_row = min([a for a in unplayed_rows if a > row_number])
except ValueError:
next_row = min(unplayed_rows)
if next_row is not None:
self.set_next_row(next_row)
def data( def data(
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
@ -415,7 +417,7 @@ class PlaylistModel(QAbstractTableModel):
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:
column_span = 1 column_span = 1
if header_row: if header_row:
column_span = self.columnCount() - 1 column_span = self.columnCount() - HEADER_NOTES_COLUMN
self.signals.span_cells_signal.emit( self.signals.span_cells_signal.emit(
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
) )
@ -532,7 +534,7 @@ class PlaylistModel(QAbstractTableModel):
Col.ARTIST.value, Col.ARTIST.value,
Col.NOTE.value, Col.NOTE.value,
Col.INTRO.value, Col.INTRO.value,
]: ] or self.is_header_row(index.row()) and index.column() == HEADER_NOTES_COLUMN:
return default | Qt.ItemFlag.ItemIsEditable return default | Qt.ItemFlag.ItemIsEditable
return default return default
@ -763,8 +765,6 @@ class PlaylistModel(QAbstractTableModel):
Signal to view to refresh invalidated row Signal to view to refresh invalidated row
""" """
log.debug(f"issue285: {self}: invalidate_row({modified_row=})")
self.dataChanged.emit( self.dataChanged.emit(
self.index(modified_row, 0), self.index(modified_row, 0),
self.index(modified_row, self.columnCount() - 1), self.index(modified_row, self.columnCount() - 1),
@ -776,8 +776,6 @@ class PlaylistModel(QAbstractTableModel):
Signal to view to refresh invlidated rows Signal to view to refresh invlidated rows
""" """
log.debug(f"issue285: {self}: invalidate_rows({modified_rows=})")
for modified_row in modified_rows: for modified_row in modified_rows:
# only invalidate required roles # only invalidate required roles
self.invalidate_row(modified_row, roles) self.invalidate_row(modified_row, roles)
@ -1199,8 +1197,6 @@ class PlaylistModel(QAbstractTableModel):
looking up the playlistrow_id and retrieving the row number from the database. looking up the playlistrow_id and retrieving the row number from the database.
""" """
log.debug(f"issue285: {self}: reset_track_sequence_row_numbers()")
# 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:
@ -1665,8 +1661,6 @@ class PlaylistModel(QAbstractTableModel):
Update track start/end times in self.playlist_rows Update track start/end times in self.playlist_rows
""" """
log.debug(f"issue285: {self}: update_track_times()")
next_start_time: Optional[dt.datetime] = None next_start_time: Optional[dt.datetime] = None
update_rows: list[int] = [] update_rows: list[int] = []
row_count = len(self.playlist_rows) row_count = len(self.playlist_rows)

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 ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo from classes import ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog
@ -308,13 +307,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()
@ -546,8 +538,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",
@ -657,8 +649,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"""
@ -860,10 +852,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.musicmuster.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))
@ -920,15 +912,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,9 @@ 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",
"slugify>=0.0.1",
] ]
[dependency-groups] [dependency-groups]
@ -63,6 +66,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"]

781
uv.lock

File diff suppressed because it is too large Load Diff