Manual fallback

This commit is contained in:
2026-03-13 20:27:50 -04:00
parent cbd4a8ee30
commit 05654a8203
2 changed files with 118 additions and 21 deletions

View File

@@ -2,12 +2,13 @@ import re
import urllib.parse import urllib.parse
import functools import functools
import unicodedata import unicodedata
from PyQt5 import QtWidgets from PyQt5 import QtWidgets, QtCore
from picard import log, config from picard import log, config
from picard.webservice import ratecontrol from picard.webservice import ratecontrol
from picard.metadata import register_album_metadata_processor from picard.metadata import register_album_metadata_processor
from picard.ui.options import register_options_page, OptionsPage from picard.ui.options import register_options_page, OptionsPage
from picard.ui.itemviews import register_album_action, BaseAction
from picard.album import Album from picard.album import Album
from picard.metadata import Metadata from picard.metadata import Metadata
@@ -75,6 +76,26 @@ class DiscogsGenreProcessor:
def __init__(self): def __init__(self):
self.host = "api.discogs.com" self.host = "api.discogs.com"
def _add_discogs_tags(self, metadata: Metadata, genres: list[str], styles: list[str]) -> None:
metadata.delete('genre')
for genre in genres:
metadata.add('genre', genre)
style_tag = config.setting["discogs_style_tag"]
if style_tag is not None and style_tag != "":
tag_name = style_tag or "grouping"
metadata.delete(tag_name)
for style in styles:
metadata.add(tag_name, style)
def _propagate_tags_to_album_tracks(self, album: Album, genres: list[str], styles: list[str]) -> None:
for track in album.tracks:
self._add_discogs_tags(track.metadata, genres, styles)
for file in track.files:
self._add_discogs_tags(file.metadata, genres, styles)
file.update()
track.update()
def _clean_search_text(self, text: str) -> str: def _clean_search_text(self, text: str) -> str:
if not text: if not text:
return "" return ""
@@ -257,7 +278,7 @@ class DiscogsGenreProcessor:
attempts: list[dict[str, str]], attempts: list[dict[str, str]],
attempt_index: int, attempt_index: int,
response, response,
reply, _reply,
error, error,
): ):
try: try:
@@ -286,12 +307,7 @@ class DiscogsGenreProcessor:
genres = valid_result.get('genre', []) genres = valid_result.get('genre', [])
styles = valid_result.get('style', []) styles = valid_result.get('style', [])
for genre in genres: self._add_discogs_tags(metadata, genres, styles)
metadata.add('genre', genre)
if config.setting["discogs_style_tag"] is not None and config.setting["discogs_style_tag"] != "":
for style in styles:
metadata.add(config.setting["discogs_style_tag"] or "grouping", style)
finally: finally:
album._requests -= 1 album._requests -= 1
if album._requests == 0: if album._requests == 0:
@@ -328,7 +344,15 @@ class DiscogsGenreProcessor:
return None return None
def fetch_discogs_tags(self, album: Album, metadata: Metadata, entity_type: str, entity_id: str, token: str): def fetch_discogs_tags(
self,
album: Album,
metadata: Metadata,
entity_type: str,
entity_id: str,
token: str,
use_album_requests: bool = True,
):
path = f"/{entity_type}s/{entity_id}" path = f"/{entity_type}s/{entity_id}"
if token: if token:
path += f"?token={token}" path += f"?token={token}"
@@ -339,10 +363,10 @@ class DiscogsGenreProcessor:
url=full_url, url=full_url,
parse_response_type="json", parse_response_type="json",
priority=True, priority=True,
handler=functools.partial(self.handle_tags_response, album, metadata) handler=functools.partial(self.handle_tags_response, album, metadata, use_album_requests)
) )
def handle_tags_response(self, album: Album, metadata: Metadata, response, reply, error): def handle_tags_response(self, album: Album, metadata: Metadata, use_album_requests: bool, response, reply, error):
try: try:
if error or not response: if error or not response:
log.error(f"Discogs Tags API failed: {error}") log.error(f"Discogs Tags API failed: {error}")
@@ -351,17 +375,90 @@ class DiscogsGenreProcessor:
genres = response.get('genres', []) genres = response.get('genres', [])
styles = response.get('styles', []) styles = response.get('styles', [])
for genre in genres: self._add_discogs_tags(metadata, genres, styles)
metadata.add('genre', genre)
if config.setting["discogs_style_tag"] is not None and config.setting["discogs_style_tag"] != "": if not use_album_requests:
for style in styles: self._propagate_tags_to_album_tracks(album, genres, styles)
metadata.add(config.setting["discogs_style_tag"] or "grouping", style)
finally: finally:
album._requests -= 1 if use_album_requests:
if album._requests == 0: if album._requests > 0:
album._requests -= 1
else:
log.warning("Discogs tags response received with no pending album requests")
if use_album_requests and not album.loaded and album._requests == 0:
album._finalize_loading(None) album._finalize_loading(None)
elif (use_album_requests and album.loaded and album._requests == 0) or not use_album_requests:
album.update()
class DiscogsManualSearchAction(BaseAction):
NAME = "[Discogs] Manual Search"
def __init__(self):
super().__init__()
def callback(self, objs):
albums = [a for a in objs if isinstance(a, Album)]
if not albums:
return
for album in albums:
title = album.metadata.get('album', '') if getattr(album, 'metadata', None) else ''
prompt = "Enter Discogs master/release code"
if title:
prompt = f"{prompt} for \"{title}\""
prompt = f"{prompt}\nFound in the top right corner of the page, example: [m123456] or [r123456]"
parent = QtWidgets.QApplication.activeWindow()
input_dialog = QtWidgets.QInputDialog(parent)
input_dialog.setWindowTitle("Discogs Manual Search")
input_dialog.setLabelText(prompt)
input_dialog.setInputMode(QtWidgets.QInputDialog.TextInput)
input_dialog.setTextValue("")
def focus_input() -> None:
line_edit = input_dialog.findChild(QtWidgets.QLineEdit)
if line_edit:
line_edit.setFocus()
QtCore.QTimer.singleShot(0, focus_input)
ok = input_dialog.exec_()
value = input_dialog.textValue()
if not ok or not value.strip():
continue
value = value.strip()
match = re.match(r'^\[[mMrR](\d+)\]$', value)
if match:
value = value[1:-1]
if not match:
QtWidgets.QMessageBox.warning(
parent,
"Invalid code",
"Please enter a valid Discogs master/release code, example: [m123456] or [r123456]",
)
continue
entity_id = match.group(1)
entity_type = 'master' if value.strip().lower().startswith('m') else 'release'
token = (config.setting["discogs_personal_access_token"] or "").strip()
DiscogsGenreProcessor().fetch_discogs_tags(
album,
album.metadata,
entity_type,
entity_id,
token,
use_album_requests=False,
)
register_options_page(DiscogsGenreOptionsPage) register_options_page(DiscogsGenreOptionsPage)
register_album_action(DiscogsManualSearchAction())
register_album_metadata_processor(DiscogsGenreProcessor().process_album) register_album_metadata_processor(DiscogsGenreProcessor().process_album)

View File

@@ -4,7 +4,7 @@ from picard.config import TextOption, IntOption, Option
PLUGIN_NAME = "Discogs Genre & Style" PLUGIN_NAME = "Discogs Genre & Style"
PLUGIN_AUTHOR = "cy1der" PLUGIN_AUTHOR = "cy1der"
PLUGIN_DESCRIPTION = "Fetches genres and styles from Discogs" PLUGIN_DESCRIPTION = "Fetches genres and styles from Discogs"
PLUGIN_VERSION = "1.0.2" PLUGIN_VERSION = "1.0.3"
PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13"] PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13"]
PLUGIN_LICENSE = "GPL-2.0-or-later" 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"