Compatibility with Python <3.9, remove CMD window pop-ups on Windows, add missing gaia dll

This commit is contained in:
2025-08-07 20:43:30 -04:00
parent 6bda2d471f
commit a31f2ad341
3 changed files with 35 additions and 27 deletions

View File

@@ -5,6 +5,7 @@ import hashlib
import threading import threading
import concurrent.futures import concurrent.futures
import tempfile import tempfile
from typing import List, Tuple, Dict, Optional
from picard import config, log from picard import config, log
from picard.ui.itemviews import ( from picard.ui.itemviews import (
@@ -15,7 +16,6 @@ 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 picard.formats import OggOpusFile
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from .constants import * from .constants import *
@@ -31,7 +31,7 @@ class AcousticBrainzNG:
binary_path += '.exe' binary_path += '.exe'
return binary_path return binary_path
def _get_binary_paths(self) -> tuple[str, str]: def _get_binary_paths(self) -> Tuple[str, str]:
binaries_path = config.setting["acousticbrainz_ng_binaries_path"] binaries_path = config.setting["acousticbrainz_ng_binaries_path"]
if not binaries_path: if not binaries_path:
raise ValueError("Binaries path not configured") raise ValueError("Binaries path not configured")
@@ -46,7 +46,7 @@ class AcousticBrainzNG:
return musicnn_binary_path, gaia_binary_path return musicnn_binary_path, gaia_binary_path
def _run_musicnn_models(self, models: list[tuple[str, str]], musicnn_binary_path: str, file: str, output_path: str) -> None: def _run_musicnn_models(self, models: List[Tuple[str, str]], musicnn_binary_path: str, file: str, output_path: str) -> None:
models_path = config.setting["acousticbrainz_ng_models_path"] models_path = config.setting["acousticbrainz_ng_models_path"]
if not models_path: if not models_path:
raise ValueError("Models path not configured") raise ValueError("Models path not configured")
@@ -68,7 +68,8 @@ class AcousticBrainzNG:
[musicnn_binary_path, model_path, file, output_file_path], [musicnn_binary_path, model_path, file, output_file_path],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
except FileNotFoundError as e: except FileNotFoundError as e:
log.error(f"Model {model_name} not found: {e}") log.error(f"Model {model_name} not found: {e}")
@@ -80,7 +81,7 @@ class AcousticBrainzNG:
futures = [executor.submit(run_musicnn_model, model) for model in models] futures = [executor.submit(run_musicnn_model, model) for model in models]
concurrent.futures.wait(futures) concurrent.futures.wait(futures)
def analyze_required(self, metadata: dict, file: str) -> None: def analyze_required(self, metadata: Dict, file: str) -> None:
if not self._check_binaries(): if not self._check_binaries():
log.error("Essentia binaries not found") log.error("Essentia binaries not found")
return return
@@ -107,7 +108,8 @@ class AcousticBrainzNG:
[gaia_binary_path, file, os.path.join(output_path, "gaia.json")], [gaia_binary_path, file, os.path.join(output_path, "gaia.json")],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
gaia_thread = threading.Thread(target=run_gaia) gaia_thread = threading.Thread(target=run_gaia)
@@ -116,7 +118,7 @@ class AcousticBrainzNG:
self._run_musicnn_models(REQUIRED_MODELS, musicnn_binary_path, file, output_path) self._run_musicnn_models(REQUIRED_MODELS, musicnn_binary_path, file, output_path)
gaia_thread.join() gaia_thread.join()
def analyze_optional(self, metadata: dict, file: str) -> None: def analyze_optional(self, metadata: Dict, file: str) -> None:
if not self._check_binaries(): if not self._check_binaries():
log.error("Essentia binaries not found") log.error("Essentia binaries not found")
return return
@@ -137,7 +139,7 @@ class AcousticBrainzNG:
self._run_musicnn_models(OPTIONAL_MODELS, musicnn_binary_path, file, output_path) self._run_musicnn_models(OPTIONAL_MODELS, musicnn_binary_path, file, output_path)
def parse_required(self, metadata: dict, file: str) -> None: def parse_required(self, metadata: Dict, file: str) -> None:
if not self._check_required_models(): if not self._check_required_models():
raise ValueError("Required models not found") raise ValueError("Required models not found")
@@ -251,7 +253,7 @@ class AcousticBrainzNG:
metadata["ab:lo:tonal:key_scale"] = selected_key_data["scale"] metadata["ab:lo:tonal:key_scale"] = selected_key_data["scale"]
metadata["ab:lo:tonal:key_key"] = selected_key_data["key"] metadata["ab:lo:tonal:key_key"] = selected_key_data["key"]
def parse_optional(self, metadata: dict, file: str) -> None: def parse_optional(self, metadata: Dict, file: str) -> None:
if not self._check_optional_models(): if not self._check_optional_models():
raise ValueError("Optional models not found") raise ValueError("Optional models not found")
@@ -303,7 +305,7 @@ class AcousticBrainzNG:
def _format_class(class_name: str) -> str: def _format_class(class_name: str) -> str:
return class_name.replace("non", "not").replace("_", " ").capitalize() return class_name.replace("non", "not").replace("_", " ").capitalize()
def _generate_cache_folder(self, metadata: dict, file_path: str) -> str: def _generate_cache_folder(self, metadata: Dict, file_path: str) -> str:
cache_base = config.setting["acousticbrainz_ng_cache_path"] cache_base = config.setting["acousticbrainz_ng_cache_path"]
if not cache_base: if not cache_base:
raise ValueError("Cache path not configured") raise ValueError("Cache path not configured")
@@ -340,7 +342,8 @@ class AcousticBrainzNG:
[binary_path, file_path], [binary_path, file_path],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
if result.returncode == 0: if result.returncode == 0:
@@ -368,7 +371,7 @@ class AcousticBrainzNG:
return True return True
def _check_models(self, models: list[tuple[str, str]]) -> bool: def _check_models(self, models: List[Tuple[str, str]]) -> bool:
path = config.setting["acousticbrainz_ng_models_path"] path = config.setting["acousticbrainz_ng_models_path"]
if not path or not os.path.exists(path): if not path or not os.path.exists(path):
@@ -390,7 +393,7 @@ class AcousticBrainzNG:
def _is_opus_file(self, file_path: str) -> bool: def _is_opus_file(self, file_path: str) -> bool:
return file_path.lower().endswith('.opus') return file_path.lower().endswith('.opus')
def calculate_track_loudness(self, file_path: str) -> dict: def calculate_track_loudness(self, file_path: str) -> Dict:
try: try:
ffmpeg_path = config.setting["acousticbrainz_ng_ffmpeg_path"] ffmpeg_path = config.setting["acousticbrainz_ng_ffmpeg_path"]
if not ffmpeg_path: if not ffmpeg_path:
@@ -400,7 +403,8 @@ class AcousticBrainzNG:
[ffmpeg_path, "-hide_banner", "-i", file_path, "-af", f"loudnorm=I={config.setting['acousticbrainz_ng_replaygain_reference_loudness']}:print_format=json", "-f", "null", "-"], [ffmpeg_path, "-hide_banner", "-i", file_path, "-af", f"loudnorm=I={config.setting['acousticbrainz_ng_replaygain_reference_loudness']}:print_format=json", "-f", "null", "-"],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
replaygain_gain = None replaygain_gain = None
@@ -430,7 +434,7 @@ class AcousticBrainzNG:
except (json.JSONDecodeError, ValueError, TypeError): except (json.JSONDecodeError, ValueError, TypeError):
pass pass
result: dict = { result: Dict = {
"replaygain_track_gain": replaygain_gain, "replaygain_track_gain": replaygain_gain,
"replaygain_track_peak": replaygain_peak, "replaygain_track_peak": replaygain_peak,
"replaygain_track_range": replaygain_range, "replaygain_track_range": replaygain_range,
@@ -441,7 +445,8 @@ class AcousticBrainzNG:
[ffmpeg_path, "-hide_banner", "-i", file_path, "-af", "loudnorm=I=-23:print_format=json", "-f", "null", "-"], [ffmpeg_path, "-hide_banner", "-i", file_path, "-af", "loudnorm=I=-23:print_format=json", "-f", "null", "-"],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
r128_track_gain = None r128_track_gain = None
@@ -475,7 +480,7 @@ class AcousticBrainzNG:
log.error(f"Error calculating track loudness: {e}") log.error(f"Error calculating track loudness: {e}")
return {} return {}
def calculate_album_loudness(self, album_track_files: list[str]) -> dict: def calculate_album_loudness(self, album_track_files: List[str]) -> Dict:
try: try:
if len(album_track_files) == 0: if len(album_track_files) == 0:
return {} return {}
@@ -499,7 +504,8 @@ class AcousticBrainzNG:
"-vn", "-af", f"loudnorm=I={config.setting['acousticbrainz_ng_replaygain_reference_loudness']}:print_format=json", "-f", "null", "-"], "-vn", "-af", f"loudnorm=I={config.setting['acousticbrainz_ng_replaygain_reference_loudness']}:print_format=json", "-f", "null", "-"],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
album_gain = None album_gain = None
@@ -529,7 +535,7 @@ class AcousticBrainzNG:
except (json.JSONDecodeError, ValueError, TypeError): except (json.JSONDecodeError, ValueError, TypeError):
pass pass
result: dict = { result: Dict = {
"replaygain_album_gain": album_gain, "replaygain_album_gain": album_gain,
"replaygain_album_peak": album_peak, "replaygain_album_peak": album_peak,
"replaygain_album_range": album_range "replaygain_album_range": album_range
@@ -540,7 +546,8 @@ class AcousticBrainzNG:
"-vn", "-af", "loudnorm=I=-23:print_format=json", "-f", "null", "-"], "-vn", "-af", "loudnorm=I=-23:print_format=json", "-f", "null", "-"],
capture_output=True, capture_output=True,
text=True, text=True,
env=ENV env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
) )
r128_album_gain = None r128_album_gain = None
@@ -577,7 +584,7 @@ class AcousticBrainzNG:
log.error(f"Error calculating album loudness: {e}") log.error(f"Error calculating album loudness: {e}")
return {} return {}
def calculate_loudness(self, metadata: dict, file_path: str, album: Album | None = None) -> None: def calculate_loudness(self, metadata: Dict, file_path: str, album: Optional[Album] = None) -> None:
try: try:
cache_folder = self._generate_cache_folder(metadata, file_path) cache_folder = self._generate_cache_folder(metadata, file_path)
loudness_file = os.path.join(cache_folder, f"loudness_{config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18}.json") loudness_file = os.path.join(cache_folder, f"loudness_{config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18}.json")
@@ -642,7 +649,7 @@ class AcousticBrainzNG:
except Exception as e: except Exception as e:
log.error(f"Error calculating loudness: {e}") log.error(f"Error calculating loudness: {e}")
def parse_loudness(self, metadata: dict, file: str) -> None: def parse_loudness(self, metadata: Dict, file: str) -> None:
output_path = self._generate_cache_folder(metadata, file) output_path = self._generate_cache_folder(metadata, file)
if not output_path: if not output_path:
raise ValueError("Failed to generate cache folder path") raise ValueError("Failed to generate cache folder path")
@@ -705,7 +712,7 @@ acousticbrainz_ng = AcousticBrainzNG()
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: Album | None = None) -> None: def _process_track(self, track: Track, album: Optional[Album] = None) -> None:
for file in track.files: for file in track.files:
acousticbrainz_ng.analyze_required(file.metadata, file.filename) acousticbrainz_ng.analyze_required(file.metadata, file.filename)
acousticbrainz_ng.parse_required(file.metadata, file.filename) acousticbrainz_ng.parse_required(file.metadata, file.filename)
@@ -875,7 +882,7 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
missing_binaries.append(binary) missing_binaries.append(binary)
try: try:
result = subprocess.run([ffmpeg_path, "-version"], capture_output=True, text=True) result = subprocess.run([ffmpeg_path, "-version"], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
if result.returncode != 0 or "ffmpeg version" not in result.stdout: if result.returncode != 0 or "ffmpeg version" not in result.stdout:
missing_binaries.append("FFmpeg (invalid executable)") missing_binaries.append("FFmpeg (invalid executable)")
except Exception: except Exception:

BIN
bin/Qt5Core.dll Normal file

Binary file not shown.

View File

@@ -1,5 +1,6 @@
import os import os
import sys import sys
from typing import List, Tuple
from picard.config import BoolOption, TextOption, IntOption from picard.config import BoolOption, TextOption, IntOption
PLUGIN_NAME = "AcousticBrainz-ng" PLUGIN_NAME = "AcousticBrainz-ng"
@@ -23,7 +24,7 @@ PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
PLUGIN_USER_GUIDE_URL = "https://example.com" # TODO: Update with actual user guide URL PLUGIN_USER_GUIDE_URL = "https://example.com" # TODO: Update with actual user guide URL
REQUIRED_MODELS: list[tuple[str, str]] = [ REQUIRED_MODELS: List[Tuple[str, str]] = [
("msd-musicnn-1", "msd"), ("msd-musicnn-1", "msd"),
("mood_acoustic-musicnn-mtt-2", "mood_acoustic"), ("mood_acoustic-musicnn-mtt-2", "mood_acoustic"),
("mood_aggressive-musicnn-mtt-2", "mood_aggressive"), ("mood_aggressive-musicnn-mtt-2", "mood_aggressive"),
@@ -38,13 +39,13 @@ REQUIRED_MODELS: list[tuple[str, str]] = [
("voice_instrumental-musicnn-msd-2", "voice_instrumental") ("voice_instrumental-musicnn-msd-2", "voice_instrumental")
] ]
OPTIONAL_MODELS: list[tuple[str, str]] = [ OPTIONAL_MODELS: List[Tuple[str, str]] = [
("genre_electronic-musicnn-msd-2", "genre_electronic"), ("genre_electronic-musicnn-msd-2", "genre_electronic"),
("genre_rosamerica-musicnn-msd-2", "genre_rosamerica"), ("genre_rosamerica-musicnn-msd-2", "genre_rosamerica"),
("genre_tzanetakis-musicnn-msd-2", "genre_tzanetakis") ("genre_tzanetakis-musicnn-msd-2", "genre_tzanetakis")
] ]
REQUIRED_BINARIES: list[str] = [ REQUIRED_BINARIES: List[str] = [
"streaming_extractor_music", "streaming_extractor_music",
"streaming_musicnn_predict", "streaming_musicnn_predict",
"streaming_md5", "streaming_md5",