From f14137fcc2450869dc96825e81f362bdedc08cb3 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 18:04:12 -0700 Subject: [PATCH 01/18] Add BPSyncPlugin --- beets/autotag/__init__.py | 2 +- beets/autotag/hooks.py | 4 +- beets/library.py | 12 +++ beets/plugins.py | 13 ++- beetsplug/beatport.py | 33 +++---- beetsplug/bpsync.py | 192 ++++++++++++++++++++++++++++++++++++++ beetsplug/mbsync.py | 17 +--- test/test_beatport.py | 16 ++-- 8 files changed, 238 insertions(+), 51 deletions(-) create mode 100644 beetsplug/bpsync.py diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 38db0a07b8..ee1bf051c5 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -186,7 +186,7 @@ def apply_metadata(album_info, mapping): 'mb_workid', 'work_disambig', 'bpm', - 'musical_key', + 'initial_key', 'genre' ) } diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index f59aaea420..c0f0ace7be 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -180,7 +180,7 @@ def __init__(self, title, track_id, release_track_id=None, artist=None, data_url=None, media=None, lyricist=None, composer=None, composer_sort=None, arranger=None, track_alt=None, work=None, mb_workid=None, work_disambig=None, bpm=None, - musical_key=None, genre=None): + initial_key=None, genre=None): self.title = title self.track_id = track_id self.release_track_id = release_track_id @@ -206,7 +206,7 @@ def __init__(self, title, track_id, release_track_id=None, artist=None, self.mb_workid = mb_workid self.work_disambig = work_disambig self.bpm = bpm - self.musical_key = musical_key + self.initial_key = initial_key self.genre = genre # As above, work around a bug in python-musicbrainz-ngs. diff --git a/beets/library.py b/beets/library.py index 59791959d6..239d96e81b 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1648,6 +1648,18 @@ def tmpl_ifdef(self, field, trueval=u'', falseval=u''): return falseval +def apply_item_changes(lib, item, move, pretend, write): + """Store, move and write the item according to the arguments. + """ + if not pretend: + # Move the item if it's in the library. + if move and lib.directory in util.ancestry(item.path): + item.move(with_album=False) + + if write: + item.try_write() + item.store() + # Get the name of tmpl_* functions in the above class. DefaultTemplateFunctions._func_names = \ [s for s in dir(DefaultTemplateFunctions) diff --git a/beets/plugins.py b/beets/plugins.py index b0752203f1..9a0f2cc73e 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -26,7 +26,7 @@ import beets -from beets import logging +from beets import library, logging, ui, util import mediafile import six @@ -635,11 +635,11 @@ def get_artist(artists, id_key='id', name_key='name'): :param artists: Iterable of artist dicts returned by API. :type artists: list[dict] - :param id_key: Key corresponding to ``artist_id`` value. - :type id_key: str - :param name_key: Keys corresponding to values to concatenate + :param id_key: Key or index corresponding to ``artist_id`` value. + :type id_key: str or int + :param name_key: Key or index corresponding to values to concatenate for ``artist``. - :type name_key: str + :type name_key: str or int :return: Normalized artist string. :rtype: str """ @@ -649,6 +649,8 @@ def get_artist(artists, id_key='id', name_key='name'): if not artist_id: artist_id = artist[id_key] name = artist[name_key] + # Strip disambiguation number. + name = re.sub(r' \(\d+\)$', '', name) # Move articles to the front. name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I) artist_names.append(name) @@ -724,3 +726,4 @@ def track_distance(self, item, track_info): return get_distance( data_source=self.data_source, info=track_info, config=self.config ) + diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index be76b902f6..360a401730 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -29,7 +29,7 @@ import beets import beets.ui from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance -from beets.plugins import BeetsPlugin +from beets.plugins import BeetsPlugin, MetadataSourcePlugin import confuse @@ -228,6 +228,7 @@ def __init__(self, data): if 'slug' in data: self.url = "https://beatport.com/release/{0}/{1}".format( data['slug'], data['id']) + self.genre = data.get('genre') @six.python_2_unicode_compatible @@ -258,7 +259,7 @@ def __init__(self, data): .format(data['slug'], data['id']) self.track_number = data.get('trackNumber') self.bpm = data.get('bpm') - self.musical_key = six.text_type( + self.initial_key = six.text_type( (data.get('key') or {}).get('shortName') ) @@ -270,6 +271,8 @@ def __init__(self, data): class BeatportPlugin(BeetsPlugin): + data_source = 'Beatport' + def __init__(self): super(BeatportPlugin, self).__init__() self.config.add({ @@ -337,7 +340,7 @@ def album_distance(self, items, album_info, mapping): for albums. """ dist = Distance() - if album_info.data_source == 'Beatport': + if album_info.data_source == self.data_source: dist.add('source', self.config['source_weight'].as_number()) return dist @@ -346,7 +349,7 @@ def track_distance(self, item, track_info): for individual tracks. """ dist = Distance() - if track_info.data_source == 'Beatport': + if track_info.data_source == self.data_source: dist.add('source', self.config['source_weight'].as_number()) return dist @@ -435,7 +438,8 @@ def _get_album_info(self, release): day=release.release_date.day, label=release.label_name, catalognum=release.catalog_number, media=u'Digital', - data_source=u'Beatport', data_url=release.url) + data_source=self.data_source, data_url=release.url, + genre=release.genre) def _get_track_info(self, track): """Returns a TrackInfo object for a Beatport Track object. @@ -449,26 +453,15 @@ def _get_track_info(self, track): artist=artist, artist_id=artist_id, length=length, index=track.track_number, medium_index=track.track_number, - data_source=u'Beatport', data_url=track.url, - bpm=track.bpm, musical_key=track.musical_key) + data_source=self.data_source, data_url=track.url, + bpm=track.bpm, initial_key=track.initial_key, + genre=track.genre) def _get_artist(self, artists): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of Beatport release or track artists. """ - artist_id = None - bits = [] - for artist in artists: - if not artist_id: - artist_id = artist[0] - name = artist[1] - # Strip disambiguation number. - name = re.sub(r' \(\d+\)$', '', name) - # Move articles to the front. - name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I) - bits.append(name) - artist = ', '.join(bits).replace(' ,', ',') or None - return artist, artist_id + return MetadataSourcePlugin.get_artist(artists=artists, id_key=0, name_key=1) def _get_tracks(self, query): """Returns a list of TrackInfo objects for a Beatport query. diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py new file mode 100644 index 0000000000..0cd4c903ae --- /dev/null +++ b/beetsplug/bpsync.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2019, Rahul Ahuja. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Update library's tags using MusicBrainz. +""" +from __future__ import division, absolute_import, print_function + +from beets.plugins import BeetsPlugin +from beets import autotag, library, ui, util + +from .beatport import BeatportPlugin + + +class BPSyncPlugin(BeetsPlugin): + def __init__(self): + super(BPSyncPlugin, self).__init__() + self.beatport_plugin = BeatportPlugin() + self.beatport_plugin.setup() + + def commands(self): + cmd = ui.Subcommand('bpsync', help=u'update metadata from Beatport') + cmd.parser.add_option( + u'-p', + u'--pretend', + action='store_true', + help=u'show all changes but do nothing', + ) + cmd.parser.add_option( + u'-m', + u'--move', + action='store_true', + dest='move', + help=u"move files in the library directory", + ) + cmd.parser.add_option( + u'-M', + u'--nomove', + action='store_false', + dest='move', + help=u"don't move files in library", + ) + cmd.parser.add_option( + u'-W', + u'--nowrite', + action='store_false', + default=None, + dest='write', + help=u"don't write updated metadata to files", + ) + cmd.parser.add_format_option() + cmd.func = self.func + return [cmd] + + def func(self, lib, opts, args): + """Command handler for the bpsync function. + """ + move = ui.should_move(opts.move) + pretend = opts.pretend + write = ui.should_write(opts.write) + query = ui.decargs(args) + + self.singletons(lib, query, move, pretend, write) + self.albums(lib, query, move, pretend, write) + + def singletons(self, lib, query, move, pretend, write): + """Retrieve and apply info from the autotagger for items matched by + query. + """ + for item in lib.items(query + [u'singleton:true']): + if not item.mb_trackid: + self._log.info( + u'Skipping singleton with no mb_trackid: {}', item + ) + continue + + if not self.is_beatport_track(item): + self._log.info( + u'Skipping non-{} singleton: {}', + self.beatport_plugin.data_source, + item, + ) + continue + + # Apply. + track_info = self.beatport_plugin.track_for_id(item.mb_trackid) + with lib.transaction(): + autotag.apply_item_metadata(item, track_info) + library.apply_item_changes(lib, item, move, pretend, write) + + @staticmethod + def is_beatport_track(track): + return ( + track.get('data_source') == BeatportPlugin.data_source + and track.mb_trackid.isnumeric() + ) + + def get_album_tracks(self, album): + if not album.mb_albumid: + self._log.info(u'Skipping album with no mb_albumid: {}', album) + return False + if not album.mb_albumid.isnumeric(): + self._log.info( + u'Skipping album with invalid {} ID: {}', + self.beatport_plugin.data_source, + album, + ) + return False + tracks = list(album.items()) + if album.get('data_source') == self.beatport_plugin.data_source: + return tracks + if not all(self.is_beatport_track(track) for track in tracks): + self._log.info( + u'Skipping non-{} release: {}', + self.beatport_plugin.data_source, + album, + ) + return False + return tracks + + def albums(self, lib, query, move, pretend, write): + """Retrieve and apply info from the autotagger for albums matched by + query and their items. + """ + # Process matching albums. + for album in lib.albums(query): + # Do we have a valid Beatport album? + items = self.get_album_tracks(album) + if not items: + continue + + # Get the Beatport album information. + album_info = self.beatport_plugin.album_for_id(album.mb_albumid) + if not album_info: + self._log.info( + u'Release ID {} not found for album {}', + album.mb_albumid, + album, + ) + continue + + beatport_track_id_to_info = { + track.track_id: track for track in album_info.tracks + } + library_track_id_to_item = { + int(item.mb_trackid): item for item in items + } + item_to_info_mapping = { + library_track_id_to_item[track_id]: track_info + for track_id, track_info in beatport_track_id_to_info.items() + } + + self._log.info(u'applying changes to {}', album) + with lib.transaction(): + autotag.apply_metadata(album_info, item_to_info_mapping) + changed = False + # Find any changed item to apply Beatport changes to album. + any_changed_item = items[0] + for item in items: + item_changed = ui.show_model_changes(item) + changed |= item_changed + if item_changed: + any_changed_item = item + library.apply_item_changes( + lib, item, move, pretend, write + ) + + if not changed: + # No change to any item. + continue + + if not pretend: + # Update album structure to reflect an item in it. + for key in library.Album.item_keys: + album[key] = any_changed_item[key] + album.store() + + # Move album art (and any inconsistent items). + if move and lib.directory in util.ancestry(items[0].path): + self._log.debug(u'moving album {}', album) + album.move() diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index b8121d9c90..c3ef1674cf 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -27,19 +27,6 @@ MBID_REGEX = r"(\d|\w){8}-(\d|\w){4}-(\d|\w){4}-(\d|\w){4}-(\d|\w){12}" -def apply_item_changes(lib, item, move, pretend, write): - """Store, move and write the item according to the arguments. - """ - if not pretend: - # Move the item if it's in the library. - if move and lib.directory in util.ancestry(item.path): - item.move(with_album=False) - - if write: - item.try_write() - item.store() - - class MBSyncPlugin(BeetsPlugin): def __init__(self): super(MBSyncPlugin, self).__init__() @@ -103,7 +90,7 @@ def singletons(self, lib, query, move, pretend, write): # Apply. with lib.transaction(): autotag.apply_item_metadata(item, track_info) - apply_item_changes(lib, item, move, pretend, write) + library.apply_item_changes(lib, item, move, pretend, write) def albums(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for albums matched by @@ -175,7 +162,7 @@ def albums(self, lib, query, move, pretend, write): changed |= item_changed if item_changed: any_changed_item = item - apply_item_changes(lib, item, move, pretend, write) + library.apply_item_changes(lib, item, move, pretend, write) if not changed: # No change to any item. diff --git a/test/test_beatport.py b/test/test_beatport.py index 8e830df76c..fb39627f87 100644 --- a/test/test_beatport.py +++ b/test/test_beatport.py @@ -482,12 +482,12 @@ def mk_test_album(self): items[4].bpm = 123 items[5].bpm = 123 - items[0].musical_key = 'Gmin' - items[1].musical_key = 'Gmaj' - items[2].musical_key = 'Fmaj' - items[3].musical_key = 'Amin' - items[4].musical_key = 'E♭maj' - items[5].musical_key = 'Amaj' + items[0].initial_key = 'Gmin' + items[1].initial_key = 'Gmaj' + items[2].initial_key = 'Fmaj' + items[3].initial_key = 'Amin' + items[4].initial_key = 'E♭maj' + items[5].initial_key = 'Amaj' for item in items: self.lib.add(item) @@ -549,9 +549,9 @@ def test_bpm_applied(self): for track, test_track in zip(self.tracks, self.test_tracks): self.assertEqual(track.bpm, test_track.bpm) - def test_musical_key_applied(self): + def test_initial_key_applied(self): for track, test_track in zip(self.tracks, self.test_tracks): - self.assertEqual(track.musical_key, test_track.musical_key) + self.assertEqual(track.initial_key, test_track.initial_key) def test_genre_applied(self): for track, test_track in zip(self.tracks, self.test_tracks): From a1885a571bb4e63ae8d7debeaf1bc03036a3493a Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 19:03:04 -0700 Subject: [PATCH 02/18] Add documentation, fix circular import --- beets/plugins.py | 2 +- beetsplug/bpsync.py | 21 ++++++++++----------- docs/changelog.rst | 5 +++++ docs/plugins/bpsync.rst | 37 +++++++++++++++++++++++++++++++++++++ docs/plugins/index.rst | 3 ++- 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 docs/plugins/bpsync.rst diff --git a/beets/plugins.py b/beets/plugins.py index 9a0f2cc73e..ff94a123c7 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -26,7 +26,7 @@ import beets -from beets import library, logging, ui, util +from beets import logging import mediafile import six diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index 0cd4c903ae..a69c9c3d7b 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -13,7 +13,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Update library's tags using MusicBrainz. +"""Update library's tags using Beatport. """ from __future__ import division, absolute_import, print_function @@ -176,17 +176,16 @@ def albums(self, lib, query, move, pretend, write): lib, item, move, pretend, write ) - if not changed: + if not changed or pretend: # No change to any item. continue - if not pretend: - # Update album structure to reflect an item in it. - for key in library.Album.item_keys: - album[key] = any_changed_item[key] - album.store() + # Update album structure to reflect an item in it. + for key in library.Album.item_keys: + album[key] = any_changed_item[key] + album.store() - # Move album art (and any inconsistent items). - if move and lib.directory in util.ancestry(items[0].path): - self._log.debug(u'moving album {}', album) - album.move() + # Move album art (and any inconsistent items). + if move and lib.directory in util.ancestry(items[0].path): + self._log.debug(u'moving album {}', album) + album.move() diff --git a/docs/changelog.rst b/docs/changelog.rst index af9f29c1fe..b96a16cde9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -75,6 +75,11 @@ New features: :bug:`2080` * :doc:`/plugins/beatport`: Fix default assignment of the musical key. :bug:`3377` +* :doc:`/plugins/bpsync`: Add `bpsync` plugin to sync metadata changes + from the Beatport database. +* :doc:`/plugins/beatport`: Fix assignment of `genre` and rename `musical_key` + to `initial_key`. + :bug:`3387` Fixes: diff --git a/docs/plugins/bpsync.rst b/docs/plugins/bpsync.rst new file mode 100644 index 0000000000..e43e33e5a2 --- /dev/null +++ b/docs/plugins/bpsync.rst @@ -0,0 +1,37 @@ +BPSync Plugin +============= + +This plugin provides the ``bpsync`` command, which lets you fetch metadata +from Beatport for albums and tracks that already have Beatport IDs. This +is useful for updating tags as they are fixed in the Beatport database, or +when you change your mind about some config options that change how tags are +written to files. If you have a music library that is already nicely tagged by +a program that also uses Beatport, this can speed up the initial import if you +just import "as-is" and then use ``bpsync`` to get up-to-date tags that are written +to the files according to your beets configuration. + + +Usage +----- + +Enable the ``bpsync`` plugin in your configuration (see :ref:`using-plugins`) +and then run ``beet bpsync QUERY`` to fetch updated metadata for a part of your +collection (or omit the query to run over your whole library). + +This plugin treats albums and singletons (non-album tracks) separately. It +first processes all matching singletons and then proceeds on to full albums. +The same query is used to search for both kinds of entities. + +The command has a few command-line options: + +* To preview the changes that would be made without applying them, use the + ``-p`` (``--pretend``) flag. +* By default, files will be moved (renamed) according to their metadata if + they are inside your beets library directory. To disable this, use the + ``-M`` (``--nomove``) command-line option. +* If you have the ``import.write`` configuration option enabled, then this + plugin will write new metadata to files' tags. To disable this, use the + ``-W`` (``--nowrite``) option. +* To customize the output of unrecognized items, use the ``-f`` + (``--format``) option. The default output is ``format_item`` or + ``format_album`` for items and albums, respectively. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 24e427dce3..b51370612a 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -142,6 +142,7 @@ Metadata * :doc:`absubmit`: Analyse audio with the `streaming_extractor_music`_ program and submit the metadata to the AcousticBrainz server * :doc:`acousticbrainz`: Fetch various AcousticBrainz metadata * :doc:`bpm`: Measure tempo using keystrokes. +* :doc:`bpsync`: Fetch updated metadata from Beatport. * :doc:`edit`: Edit metadata from a text editor. * :doc:`embedart`: Embed album art images into files' metadata. * :doc:`fetchart`: Fetch album cover art from various sources. @@ -154,7 +155,7 @@ Metadata * :doc:`lastgenre`: Fetch genres based on Last.fm tags. * :doc:`lastimport`: Collect play counts from Last.fm. * :doc:`lyrics`: Automatically fetch song lyrics. -* :doc:`mbsync`: Fetch updated metadata from MusicBrainz +* :doc:`mbsync`: Fetch updated metadata from MusicBrainz. * :doc:`metasync`: Fetch metadata from local or remote sources * :doc:`mpdstats`: Connect to `MPD`_ and update the beets library with play statistics (last_played, play_count, skip_count, rating). From 14b8f30eadd10736dd3a8c891d586ff9a7af90d8 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 19:13:40 -0700 Subject: [PATCH 03/18] Appease flake8 --- beets/plugins.py | 1 - beetsplug/beatport.py | 4 +++- beetsplug/mbsync.py | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index ff94a123c7..738b48b5ea 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -726,4 +726,3 @@ def track_distance(self, item, track_info): return get_distance( data_source=self.data_source, info=track_info, config=self.config ) - diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index 360a401730..c2eb986657 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -461,7 +461,9 @@ def _get_artist(self, artists): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of Beatport release or track artists. """ - return MetadataSourcePlugin.get_artist(artists=artists, id_key=0, name_key=1) + return MetadataSourcePlugin.get_artist( + artists=artists, id_key=0, name_key=1 + ) def _get_tracks(self, query): """Returns a list of TrackInfo objects for a Beatport query. diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index c3ef1674cf..18474e6ca6 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -162,7 +162,9 @@ def albums(self, lib, query, move, pretend, write): changed |= item_changed if item_changed: any_changed_item = item - library.apply_item_changes(lib, item, move, pretend, write) + library.apply_item_changes( + lib, item, move, pretend, write + ) if not changed: # No change to any item. From 571e8902fd7bd223336651880b669d80050b52ec Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 19:23:29 -0700 Subject: [PATCH 04/18] Fix indentation --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b96a16cde9..963863ae88 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -78,7 +78,7 @@ New features: * :doc:`/plugins/bpsync`: Add `bpsync` plugin to sync metadata changes from the Beatport database. * :doc:`/plugins/beatport`: Fix assignment of `genre` and rename `musical_key` - to `initial_key`. + to `initial_key`. :bug:`3387` Fixes: From ca57100f27227da1acea77f867b8a195fab0cb19 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 19:55:53 -0700 Subject: [PATCH 05/18] Add `bpsync` to toctree --- docs/plugins/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index b51370612a..56864b2e09 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -65,6 +65,7 @@ following to your configuration:: beatport bpd bpm + bpsync bucket chroma convert From 5e6b8f5264022fe5cb5119453665612da1667ba0 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 22:04:30 -0700 Subject: [PATCH 06/18] DRY album/track distances --- beetsplug/beatport.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index c2eb986657..6a45ab93ab 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -28,8 +28,8 @@ import beets import beets.ui -from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance -from beets.plugins import BeetsPlugin, MetadataSourcePlugin +from beets.autotag.hooks import AlbumInfo, TrackInfo +from beets.plugins import BeetsPlugin, MetadataSourcePlugin, get_distance import confuse @@ -336,22 +336,24 @@ def _tokenfile(self): return self.config['tokenfile'].get(confuse.Filename(in_app_dir=True)) def album_distance(self, items, album_info, mapping): - """Returns the beatport source weight and the maximum source weight + """Returns the Beatport source weight and the maximum source weight for albums. """ - dist = Distance() - if album_info.data_source == self.data_source: - dist.add('source', self.config['source_weight'].as_number()) - return dist + return get_distance( + data_source=self.data_source, + info=album_info, + config=self.config + ) def track_distance(self, item, track_info): - """Returns the beatport source weight and the maximum source weight + """Returns the Beatport source weight and the maximum source weight for individual tracks. """ - dist = Distance() - if track_info.data_source == self.data_source: - dist.add('source', self.config['source_weight'].as_number()) - return dist + return get_distance( + data_source=self.data_source, + info=track_info, + config=self.config + ) def candidates(self, items, artist, release, va_likely): """Returns a list of AlbumInfo objects for beatport search results From ea03c7fac2199158b21cdcbc9013ba01afc7b9b6 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 22:16:32 -0700 Subject: [PATCH 07/18] Better readability --- beets/library.py | 17 +++++++++-------- beetsplug/bpsync.py | 3 +-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/beets/library.py b/beets/library.py index 239d96e81b..d38d3ebb5a 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1651,14 +1651,15 @@ def tmpl_ifdef(self, field, trueval=u'', falseval=u''): def apply_item_changes(lib, item, move, pretend, write): """Store, move and write the item according to the arguments. """ - if not pretend: - # Move the item if it's in the library. - if move and lib.directory in util.ancestry(item.path): - item.move(with_album=False) - - if write: - item.try_write() - item.store() + if pretend: + return + # Move the item if it's in the library. + if move and lib.directory in util.ancestry(item.path): + item.move(with_album=False) + + if write: + item.try_write() + item.store() # Get the name of tmpl_* functions in the above class. DefaultTemplateFunctions._func_names = \ diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index a69c9c3d7b..479698faf0 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -176,8 +176,7 @@ def albums(self, lib, query, move, pretend, write): lib, item, move, pretend, write ) - if not changed or pretend: - # No change to any item. + if pretend or not changed: continue # Update album structure to reflect an item in it. From 6c49afaf2241a801cf4e71de605b1e832105ecfa Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 22:49:32 -0700 Subject: [PATCH 08/18] Better naming --- beetsplug/bpsync.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index 479698faf0..ce4470611c 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -100,10 +100,10 @@ def singletons(self, lib, query, move, pretend, write): library.apply_item_changes(lib, item, move, pretend, write) @staticmethod - def is_beatport_track(track): + def is_beatport_track(item): return ( - track.get('data_source') == BeatportPlugin.data_source - and track.mb_trackid.isnumeric() + item.get('data_source') == BeatportPlugin.data_source + and item.mb_trackid.isnumeric() ) def get_album_tracks(self, album): @@ -117,17 +117,17 @@ def get_album_tracks(self, album): album, ) return False - tracks = list(album.items()) + items = list(album.items()) if album.get('data_source') == self.beatport_plugin.data_source: - return tracks - if not all(self.is_beatport_track(track) for track in tracks): + return items + if not all(self.is_beatport_track(item) for item in items): self._log.info( u'Skipping non-{} release: {}', self.beatport_plugin.data_source, album, ) return False - return tracks + return items def albums(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for albums matched by @@ -150,20 +150,20 @@ def albums(self, lib, query, move, pretend, write): ) continue - beatport_track_id_to_info = { + beatport_trackid_to_trackinfo = { track.track_id: track for track in album_info.tracks } - library_track_id_to_item = { + library_trackid_to_item = { int(item.mb_trackid): item for item in items } - item_to_info_mapping = { - library_track_id_to_item[track_id]: track_info - for track_id, track_info in beatport_track_id_to_info.items() + item_to_trackinfo = { + library_trackid_to_item[track_id]: track_info + for track_id, track_info in beatport_trackid_to_trackinfo.items() } self._log.info(u'applying changes to {}', album) with lib.transaction(): - autotag.apply_metadata(album_info, item_to_info_mapping) + autotag.apply_metadata(album_info, item_to_trackinfo) changed = False # Find any changed item to apply Beatport changes to album. any_changed_item = items[0] From 0685305efb2d873b09db532b8dfbe7426d011533 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 22:53:35 -0700 Subject: [PATCH 09/18] Only sync tracks in library --- beetsplug/bpsync.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index ce4470611c..82867ab347 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -141,8 +141,8 @@ def albums(self, lib, query, move, pretend, write): continue # Get the Beatport album information. - album_info = self.beatport_plugin.album_for_id(album.mb_albumid) - if not album_info: + albuminfo = self.beatport_plugin.album_for_id(album.mb_albumid) + if not albuminfo: self._log.info( u'Release ID {} not found for album {}', album.mb_albumid, @@ -151,19 +151,19 @@ def albums(self, lib, query, move, pretend, write): continue beatport_trackid_to_trackinfo = { - track.track_id: track for track in album_info.tracks + track.track_id: track for track in albuminfo.tracks } library_trackid_to_item = { int(item.mb_trackid): item for item in items } item_to_trackinfo = { - library_trackid_to_item[track_id]: track_info - for track_id, track_info in beatport_trackid_to_trackinfo.items() + item: beatport_trackid_to_trackinfo[track_id] + for track_id, item in library_trackid_to_item.items() } self._log.info(u'applying changes to {}', album) with lib.transaction(): - autotag.apply_metadata(album_info, item_to_trackinfo) + autotag.apply_metadata(albuminfo, item_to_trackinfo) changed = False # Find any changed item to apply Beatport changes to album. any_changed_item = items[0] From a7cdaac5f8da3856fa34a0cf313e53e06dcd5741 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Thu, 3 Oct 2019 22:57:28 -0700 Subject: [PATCH 10/18] Consistent naming --- beets/library.py | 1 + beetsplug/bpsync.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/beets/library.py b/beets/library.py index d38d3ebb5a..fd48a2ab86 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1653,6 +1653,7 @@ def apply_item_changes(lib, item, move, pretend, write): """ if pretend: return + # Move the item if it's in the library. if move and lib.directory in util.ancestry(item.path): item.move(with_album=False) diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index 82867ab347..70eb9094ae 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -94,9 +94,9 @@ def singletons(self, lib, query, move, pretend, write): continue # Apply. - track_info = self.beatport_plugin.track_for_id(item.mb_trackid) + trackinfo = self.beatport_plugin.track_for_id(item.mb_trackid) with lib.transaction(): - autotag.apply_item_metadata(item, track_info) + autotag.apply_item_metadata(item, trackinfo) library.apply_item_changes(lib, item, move, pretend, write) @staticmethod From ce90b2aae5540146df250f2c59d3ca6c04d399aa Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 15:07:20 -0700 Subject: [PATCH 11/18] Improve documentation --- beets/library.py | 14 ------------- beets/plugins.py | 46 ++++++++++++++++++++++++++++++++++++----- beetsplug/bpsync.py | 8 +++---- beetsplug/mbsync.py | 8 +++---- docs/plugins/bpsync.rst | 17 +++++++-------- 5 files changed, 54 insertions(+), 39 deletions(-) diff --git a/beets/library.py b/beets/library.py index fd48a2ab86..59791959d6 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1648,20 +1648,6 @@ def tmpl_ifdef(self, field, trueval=u'', falseval=u''): return falseval -def apply_item_changes(lib, item, move, pretend, write): - """Store, move and write the item according to the arguments. - """ - if pretend: - return - - # Move the item if it's in the library. - if move and lib.directory in util.ancestry(item.path): - item.move(with_album=False) - - if write: - item.try_write() - item.store() - # Get the name of tmpl_* functions in the above class. DefaultTemplateFunctions._func_names = \ [s for s in dir(DefaultTemplateFunctions) diff --git a/beets/plugins.py b/beets/plugins.py index 738b48b5ea..c688b058f1 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -206,7 +206,7 @@ def add_media_field(self, name, descriptor): ``descriptor`` must be an instance of ``mediafile.MediaField``. """ - # Defer impor to prevent circular dependency + # Defer import to prevent circular dependency from beets import library mediafile.MediaFile.add_field(name, descriptor) library.Item._media_fields.add(name) @@ -590,6 +590,36 @@ def get_distance(config, data_source, info): return dist +def apply_item_changes(lib, item, move, pretend, write): + """Store, move, and write the item according to the arguments. + + :param lib: beets library. + :type lib: beets.library.Library + :param item: Item whose changes to apply. + :type item: beets.library.Item + :param move: Move the item if it's in the library. + :type move: bool + :param pretend: Return without moving, writing, or storing the item's + metadata. + :type pretend: bool + :param write: Write the item's metadata to its media file. + :type write: bool + """ + if pretend: + return + + from beets import util + + # Move the item if it's in the library. + if move and lib.directory in util.ancestry(item.path): + item.move(with_album=False) + + if write: + item.try_write() + + item.store() + + @six.add_metaclass(abc.ABCMeta) class MetadataSourcePlugin(object): def __init__(self): @@ -633,12 +663,18 @@ def get_artist(artists, id_key='id', name_key='name'): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of artist object dicts. - :param artists: Iterable of artist dicts returned by API. - :type artists: list[dict] - :param id_key: Key or index corresponding to ``artist_id`` value. + For each artist, this function moves articles (such as 'a', 'an', + and 'the') to the front and strips trailing disambiguation numbers. It + returns a tuple of containing the space-separated string of all + normalized artists and the ``id`` of the main artist. + + :param artists: Iterable of artist dicts or lists returned by API. + :type artists: list[dict] or list[list] + :param id_key: Key or index corresponding to ``artist_id`` + value (the main artist). :type id_key: str or int :param name_key: Key or index corresponding to values to concatenate - for ``artist``. + for the artist string (all artists) :type name_key: str or int :return: Normalized artist string. :rtype: str diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index 70eb9094ae..aefb1517b6 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -17,7 +17,7 @@ """ from __future__ import division, absolute_import, print_function -from beets.plugins import BeetsPlugin +from beets.plugins import BeetsPlugin, apply_item_changes from beets import autotag, library, ui, util from .beatport import BeatportPlugin @@ -97,7 +97,7 @@ def singletons(self, lib, query, move, pretend, write): trackinfo = self.beatport_plugin.track_for_id(item.mb_trackid) with lib.transaction(): autotag.apply_item_metadata(item, trackinfo) - library.apply_item_changes(lib, item, move, pretend, write) + apply_item_changes(lib, item, move, pretend, write) @staticmethod def is_beatport_track(item): @@ -172,9 +172,7 @@ def albums(self, lib, query, move, pretend, write): changed |= item_changed if item_changed: any_changed_item = item - library.apply_item_changes( - lib, item, move, pretend, write - ) + apply_item_changes(lib, item, move, pretend, write) if pretend or not changed: continue diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 18474e6ca6..a2b3bc4aa4 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -17,7 +17,7 @@ """ from __future__ import division, absolute_import, print_function -from beets.plugins import BeetsPlugin +from beets.plugins import BeetsPlugin, apply_item_changes from beets import autotag, library, ui, util from beets.autotag import hooks from collections import defaultdict @@ -90,7 +90,7 @@ def singletons(self, lib, query, move, pretend, write): # Apply. with lib.transaction(): autotag.apply_item_metadata(item, track_info) - library.apply_item_changes(lib, item, move, pretend, write) + apply_item_changes(lib, item, move, pretend, write) def albums(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for albums matched by @@ -162,9 +162,7 @@ def albums(self, lib, query, move, pretend, write): changed |= item_changed if item_changed: any_changed_item = item - library.apply_item_changes( - lib, item, move, pretend, write - ) + apply_item_changes(lib, item, move, pretend, write) if not changed: # No change to any item. diff --git a/docs/plugins/bpsync.rst b/docs/plugins/bpsync.rst index e43e33e5a2..8c576e816f 100644 --- a/docs/plugins/bpsync.rst +++ b/docs/plugins/bpsync.rst @@ -2,13 +2,13 @@ BPSync Plugin ============= This plugin provides the ``bpsync`` command, which lets you fetch metadata -from Beatport for albums and tracks that already have Beatport IDs. This -is useful for updating tags as they are fixed in the Beatport database, or -when you change your mind about some config options that change how tags are -written to files. If you have a music library that is already nicely tagged by -a program that also uses Beatport, this can speed up the initial import if you -just import "as-is" and then use ``bpsync`` to get up-to-date tags that are written -to the files according to your beets configuration. +from Beatport for albums and tracks that already have Beatport IDs. +This plugins works similarly to :doc:`/plugins/mbsync`. + +If you have purchased music from Beatport, this can speed +up the initial import if you just import "as-is" and then use ``bpsync`` to +get up-to-date tags that are written to the files according to your beets +configuration. Usage @@ -32,6 +32,3 @@ The command has a few command-line options: * If you have the ``import.write`` configuration option enabled, then this plugin will write new metadata to files' tags. To disable this, use the ``-W`` (``--nowrite``) option. -* To customize the output of unrecognized items, use the ``-f`` - (``--format``) option. The default output is ``format_item`` or - ``format_album`` for items and albums, respectively. From e2c63d490149356d325a0a40fccdd6bf9a8e0ca9 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 15:25:48 -0700 Subject: [PATCH 12/18] Improve docstring wording --- beets/plugins.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index c688b058f1..635dd9ca2a 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -666,15 +666,15 @@ def get_artist(artists, id_key='id', name_key='name'): For each artist, this function moves articles (such as 'a', 'an', and 'the') to the front and strips trailing disambiguation numbers. It returns a tuple of containing the space-separated string of all - normalized artists and the ``id`` of the main artist. + normalized artists and the ``id`` of the main/first artist. :param artists: Iterable of artist dicts or lists returned by API. :type artists: list[dict] or list[list] - :param id_key: Key or index corresponding to ``artist_id`` - value (the main artist). + :param id_key: Key or index corresponding to the value of ``id`` for + the main/first artist. :type id_key: str or int - :param name_key: Key or index corresponding to values to concatenate - for the artist string (all artists) + :param name_key: Key or index corresponding to values of names + to concatenate for the artist string (containing all artists). :type name_key: str or int :return: Normalized artist string. :rtype: str From a07972d02a42d2280b1eae9b99d33d7ea2b0715b Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 15:53:41 -0700 Subject: [PATCH 13/18] Fix docstring wording --- beets/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/plugins.py b/beets/plugins.py index 635dd9ca2a..fd539934eb 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -665,7 +665,7 @@ def get_artist(artists, id_key='id', name_key='name'): For each artist, this function moves articles (such as 'a', 'an', and 'the') to the front and strips trailing disambiguation numbers. It - returns a tuple of containing the space-separated string of all + returns a tuple of containing the comma-separated string of all normalized artists and the ``id`` of the main/first artist. :param artists: Iterable of artist dicts or lists returned by API. From 19f045290ecd1636dbffb95032ad33cc8515b881 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 15:55:45 -0700 Subject: [PATCH 14/18] Fix docstring wording --- beets/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/plugins.py b/beets/plugins.py index fd539934eb..f4c78d55da 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -665,7 +665,7 @@ def get_artist(artists, id_key='id', name_key='name'): For each artist, this function moves articles (such as 'a', 'an', and 'the') to the front and strips trailing disambiguation numbers. It - returns a tuple of containing the comma-separated string of all + returns a tuple containing the comma-separated string of all normalized artists and the ``id`` of the main/first artist. :param artists: Iterable of artist dicts or lists returned by API. From 52961d71f59d53fc98273729dc828b240ebb92b0 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 16:30:24 -0700 Subject: [PATCH 15/18] Add defaults to docstring --- beets/plugins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/plugins.py b/beets/plugins.py index f4c78d55da..d85f53f650 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -671,10 +671,11 @@ def get_artist(artists, id_key='id', name_key='name'): :param artists: Iterable of artist dicts or lists returned by API. :type artists: list[dict] or list[list] :param id_key: Key or index corresponding to the value of ``id`` for - the main/first artist. + the main/first artist. Defaults to 'id'. :type id_key: str or int :param name_key: Key or index corresponding to values of names to concatenate for the artist string (containing all artists). + Defaults to 'name'. :type name_key: str or int :return: Normalized artist string. :rtype: str From 46b58ea70b4e2693685dd364462760deb658d4d3 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 20:59:47 -0700 Subject: [PATCH 16/18] Fix spelling --- docs/plugins/bpsync.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/bpsync.rst b/docs/plugins/bpsync.rst index 8c576e816f..29cbd08e3a 100644 --- a/docs/plugins/bpsync.rst +++ b/docs/plugins/bpsync.rst @@ -3,9 +3,9 @@ BPSync Plugin This plugin provides the ``bpsync`` command, which lets you fetch metadata from Beatport for albums and tracks that already have Beatport IDs. -This plugins works similarly to :doc:`/plugins/mbsync`. +This plugin works similarly to :doc:`/plugins/mbsync`. -If you have purchased music from Beatport, this can speed +If you have downloaded music from Beatport, this can speed up the initial import if you just import "as-is" and then use ``bpsync`` to get up-to-date tags that are written to the files according to your beets configuration. From 32ea225fad4e6a09f13eb96fca5248b31fe261f5 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sat, 5 Oct 2019 23:12:53 -0700 Subject: [PATCH 17/18] Guard against "empty" albums --- beetsplug/deezer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index a4337d2737..4e3fca33a6 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -83,6 +83,8 @@ def album_for_id(self, album_id): tracks_data = requests.get( self.album_url + deezer_id + '/tracks' ).json()['data'] + if not tracks_data: + return None tracks = [] medium_totals = collections.defaultdict(int) for i, track_data in enumerate(tracks_data, start=1): From 11e00c549b390f5bd527c0b4d4beedc23c70aaa3 Mon Sep 17 00:00:00 2001 From: Rahul Ahuja Date: Sun, 6 Oct 2019 00:07:23 -0700 Subject: [PATCH 18/18] Exclude empty albums from candidates --- beets/plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index d85f53f650..73d85cdd3e 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -733,8 +733,9 @@ def candidates(self, items, artist, album, va_likely): query_filters = {'album': album} if not va_likely: query_filters['artist'] = artist - albums = self._search_api(query_type='album', filters=query_filters) - return [self.album_for_id(album_id=a['id']) for a in albums] + results = self._search_api(query_type='album', filters=query_filters) + albums = [self.album_for_id(album_id=r['id']) for r in results] + return [a for a in albums if a is not None] def item_candidates(self, item, artist, title): """Returns a list of TrackInfo objects for Search API results