Keep main window responsive

This commit is contained in:
2025-08-09 15:25:58 -04:00
parent 000ff403e6
commit c515e85c18
2 changed files with 225 additions and 40 deletions

View File

@@ -5,6 +5,8 @@ import hashlib
import threading import threading
import concurrent.futures import concurrent.futures
import tempfile import tempfile
import re
from functools import partial
from typing import List, Tuple, Dict, Optional from typing import List, Tuple, Dict, Optional
from picard import config, log from picard import config, log
@@ -16,7 +18,23 @@ from picard.ui.itemviews import (
from picard.track import Track from picard.track import Track
from picard.album import Album from picard.album import Album
from picard.ui.options import OptionsPage, register_options_page from picard.ui.options import OptionsPage, register_options_page
from PyQt5 import QtWidgets from picard.util import thread
from PyQt5 import QtWidgets, QtCore
_analysis_semaphore = None
_current_max_concurrent = 0
def _get_analysis_semaphore():
global _analysis_semaphore, _current_max_concurrent
max_concurrent = config.setting["acousticbrainz_ng_max_concurrent_analyses"] or 2
if _analysis_semaphore is None or _current_max_concurrent != max_concurrent:
_analysis_semaphore = threading.Semaphore(max_concurrent)
_current_max_concurrent = max_concurrent
log.debug(f"Created analysis semaphore with limit: {max_concurrent}")
return _analysis_semaphore
from .constants import * from .constants import *
@@ -695,7 +713,6 @@ class AcousticBrainzNG:
track_loudness = self.calculate_track_loudness(file_path) track_loudness = self.calculate_track_loudness(file_path)
# Check if track loudness calculation failed
if not track_loudness: if not track_loudness:
log.error("Failed to calculate track loudness") log.error("Failed to calculate track loudness")
return False return False
@@ -722,10 +739,8 @@ class AcousticBrainzNG:
album_loudness["r128_album_gain"] = track_loudness.get("r128_track_gain") album_loudness["r128_album_gain"] = track_loudness.get("r128_track_gain")
else: else:
album_loudness = self.calculate_album_loudness(album_track_files) album_loudness = self.calculate_album_loudness(album_track_files)
# Check if album loudness calculation failed
if not album_loudness: if not album_loudness:
log.error("Failed to calculate album loudness") log.error("Failed to calculate album loudness")
# Continue with track-only data
album_data = { album_data = {
"track_count": len(album_track_files), "track_count": len(album_track_files),
@@ -748,7 +763,6 @@ class AcousticBrainzNG:
album_loudness["r128_album_gain"] = album_data.get('r128_album_gain') album_loudness["r128_album_gain"] = album_data.get('r128_album_gain')
except (FileNotFoundError, json.JSONDecodeError) as e: except (FileNotFoundError, json.JSONDecodeError) as e:
log.error(f"Error reading album data file: {e}") log.error(f"Error reading album data file: {e}")
# Continue without album loudness data
loudness_data = { loudness_data = {
**track_loudness, **track_loudness,
@@ -834,52 +848,181 @@ class AcousticBrainzNG:
acousticbrainz_ng = AcousticBrainzNG() acousticbrainz_ng = AcousticBrainzNG()
def analyze_track(track: Track, album: Optional[Album] = None) -> Dict:
results = {
'track': track,
'album': album,
'success': True,
'errors': [],
'files_processed': 0
}
semaphore = _get_analysis_semaphore()
semaphore.acquire()
try:
for file in track.files:
try:
ar_result = acousticbrainz_ng.analyze_required(file.metadata, file.filename)
pr_result = acousticbrainz_ng.parse_required(file.metadata, file.filename)
if not ar_result or not pr_result:
error_msg = f"Failed to analyze required models for {file.filename}"
log.error(error_msg)
results['errors'].append(error_msg)
results['success'] = False
continue
if config.setting["acousticbrainz_ng_analyze_optional"]:
ao_result = acousticbrainz_ng.analyze_optional(file.metadata, file.filename)
ap_result = acousticbrainz_ng.parse_optional(file.metadata, file.filename)
if not ao_result or not ap_result:
error_msg = f"Failed to analyze optional models for {file.filename}"
log.error(error_msg)
results['errors'].append(error_msg)
if config.setting["acousticbrainz_ng_calculate_replaygain"]:
cl_result = acousticbrainz_ng.calculate_loudness(file.metadata, file.filename, album)
pl_result = acousticbrainz_ng.parse_loudness(file.metadata, file.filename)
if not cl_result or not pl_result:
error_msg = f"Failed to calculate loudness for {file.filename}"
log.error(error_msg)
results['errors'].append(error_msg)
results['files_processed'] += 1
except Exception as e:
error_msg = f"Unexpected error analyzing {file.filename}: {str(e)}"
log.error(error_msg)
results['errors'].append(error_msg)
results['success'] = False
finally:
semaphore.release()
return results
class AcousticBrainzNGAction(BaseAction): class AcousticBrainzNGAction(BaseAction):
NAME = f"Analyze with {PLUGIN_NAME}" NAME = f"Analyze with {PLUGIN_NAME}"
def _process_track(self, track: Track, album: Optional[Album] = None) -> None: def __init__(self):
window = self.tagger.window super().__init__()
self.num_tracks = 0
self.current = 0
for file in track.files: def _get_pending(self) -> int:
ar_result = acousticbrainz_ng.analyze_required(file.metadata, file.filename) label = self.tagger.window.status_indicators[0].val4
pr_result = acousticbrainz_ng.parse_required(file.metadata, file.filename)
if not ar_result or not pr_result: try:
log.error(f"Failed to analyze required models for {file.filename}") pending = int(label.text() or "0")
window.set_statusbar_message("Failed to analyze required models for %s", file.filename) except ValueError:
continue m = re.search(r"(\d+)", label.text())
pending = int(m.group(1)) if m else 0
return pending
def _update_pending_count(self, delta: int):
label = self.tagger.window.status_indicators[0].val4
pending = self._get_pending()
label.setNum(pending + delta)
def _format_progress(self):
if self.num_tracks <= 1:
return ""
else:
self.current += 1
return f" ({self.current}/{self.num_tracks})"
def _analysis_callback(self, result=None, error=None):
progress = self._format_progress()
if error is None and result:
track = result['track']
album = result['album']
for file in track.files:
file.update()
track.update()
if album:
album.update()
if result['success'] and not result['errors']:
if album:
album_name = album.metadata.get('album', 'Unknown Album')
track_name = track.metadata.get('title', 'Unknown Track')
self.tagger.window.set_statusbar_message(
'Successfully analyzed "%s" from "%s"%s.', track_name, album_name, progress
)
else:
track_name = track.metadata.get('title', 'Unknown Track')
self.tagger.window.set_statusbar_message(
'Successfully analyzed "%s"%s.', track_name, progress
)
else: else:
window.set_statusbar_message("Analyzed required models for %s", file.filename) track_name = track.metadata.get('title', 'Unknown Track')
if result['files_processed'] > 0:
if config.setting["acousticbrainz_ng_analyze_optional"]: self.tagger.window.set_statusbar_message(
ao_result = acousticbrainz_ng.analyze_optional(file.metadata, file.filename) 'Partially analyzed "%s" with warnings%s.', track_name, progress
ap_result = acousticbrainz_ng.parse_optional(file.metadata, file.filename) )
if not ao_result or not ap_result:
log.error(f"Failed to analyze optional models for {file.filename}")
window.set_statusbar_message("Failed to analyze optional models for %s", file.filename)
else: else:
window.set_statusbar_message("Analyzed optional models for %s", file.filename) self.tagger.window.set_statusbar_message(
'Failed to analyze "%s"%s.', track_name, progress
)
else:
track_name = "Unknown Track"
if result and result.get('track'):
track_name = result['track'].metadata.get('title', 'Unknown Track')
if config.setting["acousticbrainz_ng_calculate_replaygain"]: error_msg = str(error) if error else "Unknown error"
cl_result = acousticbrainz_ng.calculate_loudness(file.metadata, file.filename, album) log.error(f"Analysis failed for {track_name}: {error_msg}")
pl_result = acousticbrainz_ng.parse_loudness(file.metadata, file.filename) self.tagger.window.set_statusbar_message(
'Failed to analyze "%s"%s.', track_name, progress
)
if not cl_result or not pl_result: files_count = len(result['track'].files) if result and result.get('track') else 1
log.error(f"Failed to calculate loudness for {file.filename}") self._update_pending_count(-files_count)
window.set_statusbar_message("Failed to calculate loudness for %s", file.filename)
else:
window.set_statusbar_message("Analyzed loudness for %s", file.filename)
window.set_statusbar_message("Analyzed %s with %s", file.filename, PLUGIN_NAME)
def callback(self, objs): def callback(self, objs):
for item in (t for t in objs if isinstance(t, Track) or isinstance(t, Album)): tracks_and_albums = [t for t in objs if isinstance(t, Track) or isinstance(t, Album)]
if not tracks_and_albums:
return
total_files = 0
tracks_to_process = []
for item in tracks_and_albums:
if isinstance(item, Track): if isinstance(item, Track):
self._process_track(item) total_files += len(item.files)
tracks_to_process.append((item, None))
elif isinstance(item, Album): elif isinstance(item, Album):
for track in item.tracks: for track in item.tracks:
self._process_track(track, item) total_files += len(track.files)
tracks_to_process.append((track, item))
if not tracks_to_process:
return
self.num_tracks = len(tracks_to_process)
self.current = 0
self._update_pending_count(total_files)
if self.num_tracks == 1:
track, album = tracks_to_process[0]
track_name = track.metadata.get('title', 'Unknown Track')
self.tagger.window.set_statusbar_message('Analyzing "%s" with %s...', track_name, PLUGIN_NAME)
else:
self.tagger.window.set_statusbar_message('Analyzing %i tracks with %s...', self.num_tracks, PLUGIN_NAME)
log.debug(f"Analyzing {total_files} files from {self.num_tracks} tracks with {PLUGIN_NAME}")
for track, album in tracks_to_process:
thread.run_task(
partial(analyze_track, track, album),
self._analysis_callback
)
class AcousticBrainzNGOptionsPage(OptionsPage): class AcousticBrainzNGOptionsPage(OptionsPage):
NAME = "acousticbrainz_ng" NAME = "acousticbrainz_ng"
@@ -924,12 +1067,13 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
self.calculate_replaygain_checkbox.setToolTip("Calculate ReplayGain values for the track and album") self.calculate_replaygain_checkbox.setToolTip("Calculate ReplayGain values for the track and album")
musicnn_workers_layout = QtWidgets.QHBoxLayout() musicnn_workers_layout = QtWidgets.QHBoxLayout()
concurrent_analyses_layout = QtWidgets.QHBoxLayout()
rg_reference_layout = QtWidgets.QHBoxLayout() rg_reference_layout = QtWidgets.QHBoxLayout()
musicnn_workers_label = QtWidgets.QLabel("Max MusicNN workers:", self) musicnn_workers_label = QtWidgets.QLabel("Max MusicNN processes:", self)
musicnn_workers_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) musicnn_workers_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.musicnn_workers_input = QtWidgets.QSpinBox(self) self.musicnn_workers_input = QtWidgets.QSpinBox(self)
self.musicnn_workers_input.setToolTip("Maximum number of concurrent MusicNN workers") self.musicnn_workers_input.setToolTip("Maximum number of concurrent MusicNN processes")
self.musicnn_workers_input.setRange(1, max(len(REQUIRED_MODELS), len(OPTIONAL_MODELS))) self.musicnn_workers_input.setRange(1, max(len(REQUIRED_MODELS), len(OPTIONAL_MODELS)))
self.musicnn_workers_input.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) self.musicnn_workers_input.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
@@ -937,6 +1081,17 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
musicnn_workers_layout.addStretch() musicnn_workers_layout.addStretch()
musicnn_workers_layout.addWidget(self.musicnn_workers_input) musicnn_workers_layout.addWidget(self.musicnn_workers_input)
concurrent_analyses_label = QtWidgets.QLabel("Max concurrent analyses:", self)
concurrent_analyses_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.concurrent_analyses_input = QtWidgets.QSpinBox(self)
self.concurrent_analyses_input.setToolTip("Maximum number of tracks analyzed simultaneously")
self.concurrent_analyses_input.setRange(1, 8)
self.concurrent_analyses_input.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
concurrent_analyses_layout.addWidget(concurrent_analyses_label)
concurrent_analyses_layout.addStretch()
concurrent_analyses_layout.addWidget(self.concurrent_analyses_input)
rg_reference_label = QtWidgets.QLabel("ReplayGain reference loudness (LUFS):", self) rg_reference_label = QtWidgets.QLabel("ReplayGain reference loudness (LUFS):", self)
rg_reference_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) rg_reference_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.rg_reference_input = QtWidgets.QSpinBox(self) self.rg_reference_input = QtWidgets.QSpinBox(self)
@@ -953,9 +1108,34 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
options_layout.addWidget(self.analyze_optional_checkbox) options_layout.addWidget(self.analyze_optional_checkbox)
options_layout.addWidget(self.save_raw_checkbox) options_layout.addWidget(self.save_raw_checkbox)
options_layout.addWidget(self.calculate_replaygain_checkbox) options_layout.addWidget(self.calculate_replaygain_checkbox)
options_layout.addLayout(concurrent_analyses_layout)
options_layout.addLayout(musicnn_workers_layout) options_layout.addLayout(musicnn_workers_layout)
concurrent_processes_layout = QtWidgets.QHBoxLayout()
concurrent_processes_label = QtWidgets.QLabel("Max concurrent processes:", self)
concurrent_processes_label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.concurrent_processes_display = QtWidgets.QLabel("0", self)
self.concurrent_processes_display.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.concurrent_processes_display.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
concurrent_processes_layout.addWidget(concurrent_processes_label)
concurrent_processes_layout.addStretch()
concurrent_processes_layout.addWidget(self.concurrent_processes_display)
def update_concurrent_processes():
concurrent_analyses = self.concurrent_analyses_input.value()
musicnn_workers = self.musicnn_workers_input.value()
max_processes = concurrent_analyses + (concurrent_analyses * musicnn_workers)
breakdown = f"[{concurrent_analyses} gaia processes + ({concurrent_analyses} × {musicnn_workers}) MusicNN processes]"
self.concurrent_processes_display.setText(f"{breakdown} = <span style='font-weight: bold;'>{max_processes}</span>")
self.concurrent_analyses_input.valueChanged.connect(update_concurrent_processes)
self.musicnn_workers_input.valueChanged.connect(update_concurrent_processes)
options_layout.addLayout(rg_reference_layout) options_layout.addLayout(rg_reference_layout)
options_layout.addLayout(concurrent_processes_layout)
layout.addWidget(options_group) layout.addWidget(options_group)
paths_group = QtWidgets.QGroupBox("Paths", self) paths_group = QtWidgets.QGroupBox("Paths", self)
@@ -1119,6 +1299,7 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
self.rg_reference_input.setEnabled(self.calculate_replaygain_checkbox.isChecked()) self.rg_reference_input.setEnabled(self.calculate_replaygain_checkbox.isChecked())
self.musicnn_workers_input.setValue(config.setting["acousticbrainz_ng_max_musicnn_workers"] or 4) self.musicnn_workers_input.setValue(config.setting["acousticbrainz_ng_max_musicnn_workers"] or 4)
self.concurrent_analyses_input.setValue(config.setting["acousticbrainz_ng_max_concurrent_analyses"] or 2)
self.rg_reference_input.setValue(config.setting["acousticbrainz_ng_replaygain_reference_loudness"] or -18) self.rg_reference_input.setValue(config.setting["acousticbrainz_ng_replaygain_reference_loudness"] or -18)
self.binaries_path_input.setText(config.setting["acousticbrainz_ng_binaries_path"]) self.binaries_path_input.setText(config.setting["acousticbrainz_ng_binaries_path"])
@@ -1137,6 +1318,9 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
max_workers = max(1, min(self.musicnn_workers_input.value(), max(len(REQUIRED_MODELS), len(OPTIONAL_MODELS)))) max_workers = max(1, min(self.musicnn_workers_input.value(), max(len(REQUIRED_MODELS), len(OPTIONAL_MODELS))))
config.setting["acousticbrainz_ng_max_musicnn_workers"] = max_workers config.setting["acousticbrainz_ng_max_musicnn_workers"] = max_workers
max_concurrent = max(1, min(self.concurrent_analyses_input.value(), 8))
config.setting["acousticbrainz_ng_max_concurrent_analyses"] = max_concurrent
rg_reference = max(-30, min(self.rg_reference_input.value(), -5)) rg_reference = max(-30, min(self.rg_reference_input.value(), -5))
config.setting["acousticbrainz_ng_replaygain_reference_loudness"] = rg_reference config.setting["acousticbrainz_ng_replaygain_reference_loudness"] = rg_reference

View File

@@ -60,6 +60,7 @@ CONFIG_OPTIONS = [
TextOption("setting", "acousticbrainz_ng_models_path", os.path.join(os.path.dirname(__file__), "models")), TextOption("setting", "acousticbrainz_ng_models_path", os.path.join(os.path.dirname(__file__), "models")),
TextOption("setting", "acousticbrainz_ng_cache_path", os.path.join(os.path.dirname(__file__), "cache")), TextOption("setting", "acousticbrainz_ng_cache_path", os.path.join(os.path.dirname(__file__), "cache")),
IntOption("setting", "acousticbrainz_ng_max_musicnn_workers", 4), IntOption("setting", "acousticbrainz_ng_max_musicnn_workers", 4),
IntOption("setting", "acousticbrainz_ng_max_concurrent_analyses", 2),
IntOption("setting", "acousticbrainz_ng_replaygain_reference_loudness", -18), IntOption("setting", "acousticbrainz_ng_replaygain_reference_loudness", -18),
BoolOption("setting", "acousticbrainz_ng_analyze_optional", False), BoolOption("setting", "acousticbrainz_ng_analyze_optional", False),
BoolOption("setting", "acousticbrainz_ng_save_raw", False), BoolOption("setting", "acousticbrainz_ng_save_raw", False),