Compare commits
16 Commits
fix_tracks
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8956642e05 | ||
|
|
791fad680a | ||
|
|
8c60d6a03d | ||
|
|
7391b4e61c | ||
|
|
266be281d0 | ||
|
|
ac487a5fa5 | ||
|
|
7d1bb0d3f7 | ||
|
|
e8d9cf8f00 | ||
|
|
7e7ae7dddf | ||
|
|
25cb444335 | ||
|
|
fa14fc7c52 | ||
|
|
6e51e65ba8 | ||
|
|
19b1bf3fde | ||
|
|
316b4708c6 | ||
|
|
4fd9a0381f | ||
|
|
88cce738d7 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.py diff=python
|
||||
@ -69,7 +69,8 @@ class AudacityController:
|
||||
select_status = self._send_command("SelectAll")
|
||||
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)
|
||||
log.debug(f"{export_status=}")
|
||||
self.path = ""
|
||||
|
||||
@ -132,14 +132,14 @@ class Config(object):
|
||||
TRACK_TIME_FORMAT = "%H:%M:%S"
|
||||
VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"
|
||||
VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player"
|
||||
VLC_VOLUME_DEFAULT = 75
|
||||
VLC_VOLUME_DROP3db = 65
|
||||
VLC_VOLUME_DEFAULT = 100
|
||||
VLC_VOLUME_DROP3db = 70
|
||||
WARNING_MS_BEFORE_FADE = 5500
|
||||
WARNING_MS_BEFORE_SILENCE = 5500
|
||||
WEB_ZOOM_FACTOR = 1.2
|
||||
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)
|
||||
|
||||
@ -35,6 +35,7 @@ from classes import (
|
||||
)
|
||||
from config import Config
|
||||
from helpers import (
|
||||
audio_file_extension,
|
||||
file_is_unreadable,
|
||||
get_tags,
|
||||
show_OK,
|
||||
@ -104,9 +105,7 @@ class FileImporter:
|
||||
# variable or an instance variable are effectively the same thing.
|
||||
workers: dict[str, DoTrackImport] = {}
|
||||
|
||||
def __init__(
|
||||
self, base_model: PlaylistModel, row_number: int = None
|
||||
) -> None:
|
||||
def __init__(self, base_model: PlaylistModel, row_number: int) -> None:
|
||||
"""
|
||||
Initialise the FileImporter singleton instance.
|
||||
"""
|
||||
@ -202,8 +201,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
|
||||
|
||||
@ -237,6 +237,26 @@ 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
|
||||
|
||||
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:
|
||||
"""
|
||||
- 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:
|
||||
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)
|
||||
@ -627,7 +648,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 = helpers.get_audio_metadata(self.import_file_path)
|
||||
|
||||
# Remove old file if so requested
|
||||
|
||||
@ -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
|
||||
@ -50,6 +51,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,
|
||||
@ -72,7 +81,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
|
||||
@ -94,6 +103,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)
|
||||
|
||||
56
app/jittermonitor.py
Normal file
56
app/jittermonitor.py
Normal 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,
|
||||
)
|
||||
@ -23,8 +23,8 @@ filters:
|
||||
# - function-name-1
|
||||
# - function-name-2
|
||||
musicmuster:
|
||||
- update_clocks
|
||||
- play_next
|
||||
jittermonitor: []
|
||||
|
||||
handlers:
|
||||
stderr:
|
||||
|
||||
@ -30,6 +30,7 @@ from log import log
|
||||
from models import PlaylistRows
|
||||
from vlcmanager import VLCManager
|
||||
|
||||
|
||||
# Define the VLC callback function type
|
||||
# import ctypes
|
||||
# import platform
|
||||
@ -353,21 +354,6 @@ class _Music:
|
||||
self.player.set_position(position)
|
||||
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:
|
||||
"""
|
||||
Set player position
|
||||
@ -391,17 +377,6 @@ class _Music:
|
||||
volume = Config.VLC_VOLUME_DEFAULT
|
||||
|
||||
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:
|
||||
"""Immediately stop playing"""
|
||||
|
||||
@ -63,6 +63,7 @@ from sqlalchemy.orm.session import Session
|
||||
import stackprinter # type: ignore
|
||||
|
||||
# App imports
|
||||
from audacity_controller import AudacityController
|
||||
from classes import (
|
||||
ApplicationError,
|
||||
Filter,
|
||||
@ -72,8 +73,8 @@ from classes import (
|
||||
from config import Config
|
||||
from dialogs import TrackSelectDialog
|
||||
from file_importer import FileImporter
|
||||
from helpers import ask_yes_no, 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 models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks
|
||||
from music_manager import RowAndTrack, track_sequence
|
||||
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
|
||||
import helpers
|
||||
|
||||
from jittermonitor import EventLoopJitterMonitor
|
||||
|
||||
class Current:
|
||||
base_model: PlaylistModel
|
||||
@ -1205,9 +1207,26 @@ class Window(QMainWindow):
|
||||
self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self)
|
||||
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.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 # # # # # # # # # #
|
||||
|
||||
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
||||
@ -2198,14 +2217,6 @@ class Window(QMainWindow):
|
||||
if self.return_pressed_in_error():
|
||||
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 track_sequence.current:
|
||||
track_sequence.current.fade()
|
||||
@ -2717,8 +2728,7 @@ class Window(QMainWindow):
|
||||
|
||||
# WARNING_MS_BEFORE_FADE milliseconds before fade starts, set
|
||||
# warning colour on time to silence box and enable play
|
||||
# controls. This is also a good time to re-enable the 10ms
|
||||
# timer (see play_next() and issue #223).
|
||||
# controls.
|
||||
|
||||
elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE:
|
||||
self.footer_section.frame_fade.setStyleSheet(
|
||||
@ -2726,11 +2736,6 @@ class Window(QMainWindow):
|
||||
)
|
||||
self.catch_return_key = False
|
||||
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:
|
||||
self.footer_section.frame_silent.setStyleSheet("")
|
||||
|
||||
@ -51,7 +51,7 @@ from models import db, NoteColours, Playdates, PlaylistRows, Tracks
|
||||
from music_manager import RowAndTrack, track_sequence
|
||||
|
||||
|
||||
HEADER_NOTES_COLUMN = 1
|
||||
HEADER_NOTES_COLUMN = 0
|
||||
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
|
||||
|
||||
|
||||
@ -288,7 +288,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Update Playdates in database
|
||||
log.debug(f"{self}: update playdates {track_id=}")
|
||||
Playdates(session, track_id)
|
||||
session.commit()
|
||||
|
||||
# Mark track as played in playlist
|
||||
log.debug(f"{self}: Mark track as played")
|
||||
@ -298,39 +297,42 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.refresh_row(session, plr.row_number)
|
||||
else:
|
||||
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
|
||||
roles = [
|
||||
Qt.ItemDataRole.DisplayRole
|
||||
]
|
||||
self.invalidate_row(row_number, roles)
|
||||
self.invalidate_row(track_sequence.previous.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
|
||||
self.invalidate_row(track_sequence.previous.row_number, roles)
|
||||
|
||||
# Update all other 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)
|
||||
else:
|
||||
# set_next_row() calls update_track_times(); else we call it
|
||||
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(
|
||||
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
|
||||
@ -415,7 +417,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if column == HEADER_NOTES_COLUMN:
|
||||
column_span = 1
|
||||
if header_row:
|
||||
column_span = self.columnCount() - 1
|
||||
column_span = self.columnCount() - HEADER_NOTES_COLUMN
|
||||
self.signals.span_cells_signal.emit(
|
||||
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
|
||||
)
|
||||
@ -532,7 +534,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Col.ARTIST.value,
|
||||
Col.NOTE.value,
|
||||
Col.INTRO.value,
|
||||
]:
|
||||
] or self.is_header_row(index.row()) and index.column() == HEADER_NOTES_COLUMN:
|
||||
return default | Qt.ItemFlag.ItemIsEditable
|
||||
|
||||
return default
|
||||
@ -763,8 +765,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Signal to view to refresh invalidated row
|
||||
"""
|
||||
|
||||
log.debug(f"issue285: {self}: invalidate_row({modified_row=})")
|
||||
|
||||
self.dataChanged.emit(
|
||||
self.index(modified_row, 0),
|
||||
self.index(modified_row, self.columnCount() - 1),
|
||||
@ -776,8 +776,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Signal to view to refresh invlidated rows
|
||||
"""
|
||||
|
||||
log.debug(f"issue285: {self}: invalidate_rows({modified_rows=})")
|
||||
|
||||
for modified_row in modified_rows:
|
||||
# only invalidate required 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.
|
||||
"""
|
||||
|
||||
log.debug(f"issue285: {self}: reset_track_sequence_row_numbers()")
|
||||
|
||||
# Check the track_sequence.next, current and previous plrs and
|
||||
# update the row number
|
||||
with db.Session() as session:
|
||||
@ -1665,8 +1661,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Update track start/end times in self.playlist_rows
|
||||
"""
|
||||
|
||||
log.debug(f"issue285: {self}: update_track_times()")
|
||||
|
||||
next_start_time: Optional[dt.datetime] = None
|
||||
update_rows: list[int] = []
|
||||
row_count = len(self.playlist_rows)
|
||||
|
||||
@ -34,7 +34,6 @@ from PyQt6.QtWidgets import (
|
||||
# import line_profiler
|
||||
|
||||
# App imports
|
||||
from audacity_controller import AudacityController
|
||||
from classes import ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo
|
||||
from config import Config
|
||||
from dialogs import TrackSelectDialog
|
||||
@ -308,13 +307,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()
|
||||
@ -546,8 +538,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",
|
||||
@ -657,8 +649,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"""
|
||||
@ -860,10 +852,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.musicmuster.ac.export()
|
||||
self._rescan(row_number)
|
||||
except ApplicationError as e:
|
||||
show_warning(self.musicmuster, "Audacity error", str(e))
|
||||
@ -920,15 +912,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))
|
||||
|
||||
|
||||
@ -33,6 +33,9 @@ dependencies = [
|
||||
"types-pyyaml>=6.0.12.20241230",
|
||||
"dogpile-cache>=1.3.4",
|
||||
"pdbpp>=0.10.3",
|
||||
"filetype>=1.2.0",
|
||||
"black>=25.1.0",
|
||||
"slugify>=0.0.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
@ -63,6 +66,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"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user