mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Add BPSyncPlugin
This commit is contained in:
parent
d2c194df15
commit
f14137fcc2
8 changed files with 238 additions and 51 deletions
|
|
@ -186,7 +186,7 @@ def apply_metadata(album_info, mapping):
|
|||
'mb_workid',
|
||||
'work_disambig',
|
||||
'bpm',
|
||||
'musical_key',
|
||||
'initial_key',
|
||||
'genre'
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ class TrackInfo(object):
|
|||
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 @@ class TrackInfo(object):
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -1648,6 +1648,18 @@ class DefaultTemplateFunctions(object):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from functools import wraps
|
|||
|
||||
|
||||
import beets
|
||||
from beets import logging
|
||||
from beets import library, logging, ui, util
|
||||
import mediafile
|
||||
import six
|
||||
|
||||
|
|
@ -635,11 +635,11 @@ class MetadataSourcePlugin(object):
|
|||
|
||||
: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 @@ class MetadataSourcePlugin(object):
|
|||
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 @@ class MetadataSourcePlugin(object):
|
|||
return get_distance(
|
||||
data_source=self.data_source, info=track_info, config=self.config
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ from requests_oauthlib.oauth1_session import (TokenRequestDenied, TokenMissing,
|
|||
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 @@ class BeatportRelease(BeatportObject):
|
|||
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 @@ class BeatportTrack(BeatportObject):
|
|||
.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 @@ class BeatportTrack(BeatportObject):
|
|||
|
||||
|
||||
class BeatportPlugin(BeetsPlugin):
|
||||
data_source = 'Beatport'
|
||||
|
||||
def __init__(self):
|
||||
super(BeatportPlugin, self).__init__()
|
||||
self.config.add({
|
||||
|
|
@ -337,7 +340,7 @@ class BeatportPlugin(BeetsPlugin):
|
|||
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 @@ class BeatportPlugin(BeetsPlugin):
|
|||
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 @@ class BeatportPlugin(BeetsPlugin):
|
|||
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 @@ class BeatportPlugin(BeetsPlugin):
|
|||
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.
|
||||
|
|
|
|||
192
beetsplug/bpsync.py
Normal file
192
beetsplug/bpsync.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -27,19 +27,6 @@ import re
|
|||
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 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
# 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 @@ class MBSyncPlugin(BeetsPlugin):
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -482,12 +482,12 @@ class BeatportTest(_common.TestCase, TestHelper):
|
|||
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 @@ class BeatportTest(_common.TestCase, TestHelper):
|
|||
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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue