PICARD-2398: Load recording relationships separately for huge releases

For releases with many recordings the MB API does not return recording level rels. Detect this and load the recordings in separate requests.
This commit is contained in:
Philipp Wolfer
2022-01-16 11:28:01 +01:00
parent 5e03e3a915
commit 8fa8bd81c5
2 changed files with 74 additions and 6 deletions

View File

@@ -7,7 +7,7 @@
# Copyright (C) 2008 Gary van der Merwe
# Copyright (C) 2008 Hendrik van Antwerpen
# Copyright (C) 2008 ojnkpjg
# Copyright (C) 2008-2011, 2014, 2018-2021 Philipp Wolfer
# Copyright (C) 2008-2011, 2014, 2018-2022 Philipp Wolfer
# Copyright (C) 2009 Nikolai Prokoschenko
# Copyright (C) 2011-2012 Chad Wilson
# Copyright (C) 2011-2013, 2019 Michael Wiencek
@@ -95,6 +95,9 @@ from picard.util.textencoding import asciipunct
from picard.ui.item import Item
RECORDING_QUERY_LIMIT = 100
def _create_artist_node_dict(source_node):
return {x['artist']['id']: x['artist'] for x in source_node['artist-credit']}
@@ -134,6 +137,7 @@ class Album(DataObject, Item):
self._requests = 0
self._tracks_loaded = False
self._discids = set()
self._recordings_map = {}
if discid:
self._discids.add(discid)
self._after_load_callbacks = []
@@ -249,6 +253,16 @@ class Album(DataObject, Item):
self.error_append(traceback.format_exc())
self._release_node = release_node
if config.setting['track_ars']:
# Detect if track relationships did not get loaded
try:
for medium_node in release_node['media']:
if medium_node['track-count']:
return 'relations' in medium_node['tracks'][0]['recording']
except KeyError:
pass
return True
def _release_request_finished(self, document, http, error):
@@ -277,6 +291,10 @@ class Album(DataObject, Item):
else:
try:
parsed = self._parse_release(document)
config = get_config()
if not parsed and config.setting['track_ars']:
log.debug('Recording relationships not loaded in initial request for %r, issuing separate requests' % self)
self._request_recording_relationships(config=config)
except Exception:
error = True
self.error_append(traceback.format_exc())
@@ -285,6 +303,51 @@ class Album(DataObject, Item):
if parsed or error:
self._finalize_loading(error)
def _request_recording_relationships(self, offset=0, limit=RECORDING_QUERY_LIMIT, config=None):
if not config:
config = get_config()
inc = (
'artist-rels',
'recording-rels',
'release-rels',
'url-rels',
'work-rels',
)
log.debug('Loading recording relationships for %r (offset=%i, limit=%i)' % (self, offset, limit))
self._requests += 1
self.load_task = self.tagger.mb_api.browse_recordings(
self._recordings_request_finished,
inc=inc,
release=self.id,
limit=limit,
offset=offset
)
def _recordings_request_finished(self, document, http, error):
self._requests -= 1
if error:
self.error_append(http.errorString())
self._finalize_loading(error)
else:
for recording in document.get('recordings', []):
recording_id = recording.get('id')
if recording_id:
self._recordings_map[recording_id] = recording
count = document.get('recording-count', 0)
offset = document.get('recording-offset', 0)
next_offset = offset + RECORDING_QUERY_LIMIT
if next_offset < count:
self._request_recording_relationships(offset=next_offset)
else:
self._finalize_loading(error)
def _merge_recording_relationships(self):
for medium_node in self._release_node.get('media', []):
for track_node in medium_node.get('tracks', []):
recording = self._recordings_map.get(track_node['recording']['id'])
if recording:
track_node['recording']['relations'] = recording.get('relations')
def _finalize_loading_track(self, track_node, metadata, artists, extra_metadata=None):
# As noted in `_parse_release` above, the release artist nodes
# may contain supplementary data that isn't present in track
@@ -460,6 +523,7 @@ class Album(DataObject, Item):
return
if not self._tracks_loaded:
self._merge_recording_relationships()
self._load_tracks()
if not self._requests:

View File

@@ -4,7 +4,7 @@
#
# Copyright (C) 2017 Sambhav Kothari
# Copyright (C) 2018, 2020-2021 Laurent Monin
# Copyright (C) 2018-2021 Philipp Wolfer
# Copyright (C) 2018-2022 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -191,18 +191,22 @@ class MBAPIHelper(APIHelper):
def find_artists(self, handler, **kwargs):
return self._find('artist', handler, **kwargs)
def _browse(self, entitytype, handler, inc=None, **kwargs):
def _browse(self, entitytype, handler, inc=None, queryargs=None, mblogin=False):
path_list = (entitytype, )
queryargs = kwargs
if queryargs is None:
queryargs = {}
if inc:
queryargs["inc"] = "+".join(inc)
return self.get(path_list, handler, queryargs=queryargs,
priority=True, important=True, mblogin=False,
priority=True, important=True, mblogin=mblogin,
refresh=False)
def browse_releases(self, handler, **kwargs):
inc = ("media", "labels")
return self._browse("release", handler, inc, **kwargs)
return self._browse("release", handler, inc, queryargs=kwargs)
def browse_recordings(self, handler, inc, **kwargs):
return self._browse('recording', handler, inc, queryargs=kwargs)
@staticmethod
def _xml_ratings(ratings):