From 990a79c8f34d2d4b095c1430bffeb5c52bf4fbb5 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 21 Dec 2023 17:54:34 +0100 Subject: [PATCH] PICARD-2584: Moved AcoustID recording parsing into helper class --- picard/acoustid/__init__.py | 69 +++++++++++++++-------------------- picard/acoustid/recordings.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 picard/acoustid/recordings.py diff --git a/picard/acoustid/__init__.py b/picard/acoustid/__init__.py index 6a5fe2fcb..5f468e260 100644 --- a/picard/acoustid/__init__.py +++ b/picard/acoustid/__init__.py @@ -34,10 +34,7 @@ import json from PyQt6 import QtCore from picard import log -from picard.acoustid.json_helpers import ( - max_source_count, - parse_recording, -) +from picard.acoustid.recordings import RecordingResolver from picard.config import get_config from picard.const import ( DEFAULT_FPCALC_THREADS, @@ -49,6 +46,7 @@ from picard.util import ( find_executable, win_prefix_longpath, ) +from picard.webservice.api_helpers import AcoustIdAPIHelper def get_score(node): @@ -76,7 +74,7 @@ AcoustIDTask = namedtuple('AcoustIDTask', ('file', 'next_func')) class AcoustIDClient(QtCore.QObject): - def __init__(self, acoustid_api): + def __init__(self, acoustid_api: AcoustIdAPIHelper): super().__init__() self._queue = deque() self._running = 0 @@ -108,42 +106,15 @@ class AcoustIDClient(QtCore.QObject): mparms, echo=None ) + task.next_func(doc, http, error) else: try: - recording_list = doc['recordings'] = [] status = document['status'] if status == 'ok': - results = document.get('results') or [] - for result in results: - recordings = result.get('recordings') or [] - max_sources = max_source_count(recordings) - result_score = get_score(result) - for recording in recordings: - parsed_recording = parse_recording(recording) - if parsed_recording is not None: - # Calculate a score based on result score and sources for this - # recording relative to other recordings in this result - score = min(recording.get('sources', 1) / max_sources, 1.0) * 100 - parsed_recording['score'] = score * result_score - parsed_recording['acoustid'] = result['id'] - recording_list.append(parsed_recording) - - if results: - if not recording_list: - # Set AcoustID in tags if there was no matching recording - task.file.metadata['acoustid_id'] = results[0]['id'] - task.file.update() - log.debug( - "AcoustID: Found no matching recordings for '%s'," - " setting acoustid_id tag to %r", - task.file.filename, results[0]['id'] - ) - else: - log.debug( - "AcoustID: Lookup successful for '%s' (recordings: %d)", - task.file.filename, - len(recording_list) - ) + resolver = RecordingResolver(self._acoustid_api.webservice) + resolver.resolve( + document, + partial(self._on_recording_resolve_finish, task, document, http)) else: mparms = { 'error': document['error']['message'], @@ -157,11 +128,31 @@ class AcoustIDClient(QtCore.QObject): mparms, echo=None ) + task.next_func(doc, http, error) except (AttributeError, KeyError, TypeError) as e: log.error("AcoustID: Error reading response", exc_info=True) - error = e + task.next_func(doc, http, e) - task.next_func(doc, http, error) + def _on_recording_resolve_finish(self, task, document, http, result=None, error=None): + recording_list = document['recordings'] = result + if not recording_list: + results = document.get('results') + if results: + # Set AcoustID in tags if there was no matching recording + task.file.metadata['acoustid_id'] = results[0]['id'] + task.file.update() + log.debug( + "AcoustID: Found no matching recordings for '%s'," + " setting acoustid_id tag to %r", + task.file.filename, results[0]['id'] + ) + else: + log.debug( + "AcoustID: Lookup successful for '%s' (recordings: %d)", + task.file.filename, + len(recording_list) + ) + task.next_func(document, http, error) def _lookup_fingerprint(self, task, result=None, error=None): if task.file.state == File.REMOVED: diff --git a/picard/acoustid/recordings.py b/picard/acoustid/recordings.py new file mode 100644 index 000000000..a34c62027 --- /dev/null +++ b/picard/acoustid/recordings.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2023 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from picard.acoustid.json_helpers import ( + max_source_count, + parse_recording, +) +from picard.webservice import WebService +from picard.webservice.api_helpers import MBAPIHelper + + +class RecordingResolver: + + def __init__(self, ws: WebService) -> None: + self.mbapi = MBAPIHelper(ws) + + def resolve(self, doc: dict, callback: callable) -> None: + recording_map = {} + results = doc.get('results') or [] + for result in results: + recordings = result.get('recordings') or [] + max_sources = max_source_count(recordings) + result_score = get_score(result) + for recording in recordings: + parsed_recording = parse_recording(recording) + if parsed_recording is not None: + # Calculate a score based on result score and sources for this + # recording relative to other recordings in this result + score = min(recording.get('sources', 1) / max_sources, 1.0) * 100 + parsed_recording['score'] = score * result_score + parsed_recording['acoustid'] = result.get('id') + recording_map[parsed_recording['id']] = parsed_recording + + # TODO: Load recording details for recordings without metadata + callback(recording_map.values()) + + +def get_score(node): + try: + return float(node.get('score', 1.0)) + except (TypeError, ValueError): + return 1.0