Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BPSyncPlugin #3389

Merged
merged 18 commits into from
Oct 6, 2019
2 changes: 1 addition & 1 deletion beets/autotag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def apply_metadata(album_info, mapping):
'mb_workid',
'work_disambig',
'bpm',
'musical_key',
'initial_key',
'genre'
)
}
Expand Down
4 changes: 2 additions & 2 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,20 @@ 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.
"""
rhlahuja marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down
10 changes: 6 additions & 4 deletions beets/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
rhlahuja marked this conversation as resolved.
Show resolved Hide resolved
:type name_key: str
:type name_key: str or int
:return: Normalized artist string.
:rtype: str
"""
Expand All @@ -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)
rhlahuja marked this conversation as resolved.
Show resolved Hide resolved
# Move articles to the front.
name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I)
artist_names.append(name)
Expand Down
55 changes: 26 additions & 29 deletions beetsplug/beatport.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@

import beets
import beets.ui
from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance
from beets.plugins import BeetsPlugin
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.plugins import BeetsPlugin, MetadataSourcePlugin, get_distance
import confuse


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
)

Expand All @@ -270,6 +271,8 @@ def __init__(self, data):


class BeatportPlugin(BeetsPlugin):
data_source = 'Beatport'

def __init__(self):
super(BeatportPlugin, self).__init__()
self.config.add({
Expand Down Expand Up @@ -333,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 == 'Beatport':
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 == 'Beatport':
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
Expand Down Expand Up @@ -435,7 +440,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.
Expand All @@ -449,26 +455,17 @@ 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.
Expand Down
190 changes: 190 additions & 0 deletions beetsplug/bpsync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# -*- 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 Beatport.
"""
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.
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)

@staticmethod
def is_beatport_track(item):
return (
item.get('data_source') == BeatportPlugin.data_source
and item.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
items = list(album.items())
if album.get('data_source') == self.beatport_plugin.data_source:
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 items

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.
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,
album,
)
continue

beatport_trackid_to_trackinfo = {
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 = {
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(albuminfo, item_to_trackinfo)
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 pretend or not changed:
continue

# 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()
Loading