mirror of
https://github.com/beetbox/beets.git
synced 2026-02-10 09:25:42 +01:00
commit
cd2d4c9b8a
3 changed files with 168 additions and 69 deletions
|
|
@ -1454,7 +1454,6 @@ class Album(BaseAlbum):
|
|||
for item in self.items():
|
||||
setattr(item, key, value)
|
||||
self._library.store(item)
|
||||
|
||||
else:
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
|
|
|
|||
|
|
@ -46,27 +46,6 @@ PYLAST_EXCEPTIONS = (
|
|||
pylast.NetworkError,
|
||||
)
|
||||
|
||||
def _lastfm_obj(obj):
|
||||
"""Given a beets item or album, look up the last.fm Track, Album or
|
||||
Artist object for which tags should be extracted.
|
||||
"""
|
||||
source = config['lastgenre']['source'].get()
|
||||
|
||||
if isinstance(obj, library.Album):
|
||||
if source == 'artist':
|
||||
return LASTFM.get_artist(obj.albumartist)
|
||||
else:
|
||||
return LASTFM.get_album(obj.albumartist, obj.album)
|
||||
|
||||
elif isinstance(obj, library.Item):
|
||||
if source == 'artist':
|
||||
return LASTFM.get_artist(obj.artist)
|
||||
else:
|
||||
return LASTFM.get_track(obj.artist, obj.title)
|
||||
|
||||
else:
|
||||
raise TypeError('obj should be an Album or Item')
|
||||
|
||||
def _tags_for(obj):
|
||||
"""Given a pylast entity (album or track), returns a list of
|
||||
tag names for that entity. Returns an empty list if the entity is
|
||||
|
|
@ -105,6 +84,7 @@ def _tags_to_genre(tags):
|
|||
# Just use the flat whitelist.
|
||||
return find_allowed(tags)
|
||||
|
||||
|
||||
def flatten_tree(elem, path, branches):
|
||||
"""Flatten nested lists/dictionaries into lists of strings
|
||||
(branches).
|
||||
|
|
@ -133,20 +113,75 @@ def find_parents(candidate, branches):
|
|||
continue
|
||||
return [candidate]
|
||||
|
||||
def is_allowed(genre):
|
||||
"""Returns True if the genre is present in the genre whitelist or
|
||||
False if not.
|
||||
"""
|
||||
if genre is None:
|
||||
return False
|
||||
if genre.lower() in options['whitelist']:
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_allowed(genres):
|
||||
"""Returns the first genre that is present in the genre whitelist or
|
||||
None if no genre is suitable.
|
||||
"""
|
||||
for genre in list(genres):
|
||||
if genre.lower() in options['whitelist']:
|
||||
if is_allowed(genre):
|
||||
return genre.title()
|
||||
return None
|
||||
|
||||
def fetch_genre(lastfm_obj):
|
||||
"""Returns the genre for this lastfm_obj.
|
||||
"""
|
||||
return _tags_to_genre(_tags_for(lastfm_obj))
|
||||
|
||||
def fetch_album_genre(obj):
|
||||
"""Returns the album genre for this obj. Either performs a lookup in
|
||||
lastfm or returns the cached value.
|
||||
"""
|
||||
lookup = u'album.{0}-{1}'.format(obj.albumartist, obj.album)
|
||||
if not cache.has_key(lookup):
|
||||
cache[lookup] = \
|
||||
fetch_genre(LASTFM.get_album(obj.albumartist, obj.album))
|
||||
return cache[lookup]
|
||||
|
||||
def fetch_album_artist_genre(obj):
|
||||
"""Returns the album artists genre for this obj. Either performs a lookup
|
||||
in lastfm or returns the cached value.
|
||||
"""
|
||||
lookup = u'artist.${0}'.format(obj.albumartist)
|
||||
if not cache.has_key(lookup):
|
||||
cache[lookup] = \
|
||||
fetch_genre(LASTFM.get_artist(obj.albumartist))
|
||||
return cache[lookup]
|
||||
|
||||
def fetch_artist_genre(obj):
|
||||
"""Returns the track artists genre for this obj. Either performs a lookup
|
||||
in lastfm or returns the cached value.
|
||||
"""
|
||||
lookup = u'artist.${0}'.format(obj.artist)
|
||||
if not cache.has_key(lookup):
|
||||
cache[lookup] = fetch_genre(LASTFM.get_artist(obj.artist))
|
||||
return cache[lookup]
|
||||
|
||||
def fetch_track_genre(obj):
|
||||
"""Returns the track genre for this obj. Either performs a lookup in
|
||||
lastfm or returns the cached value. """
|
||||
lookup = u'track.{0}-{1}'.format(obj.artist, obj.title)
|
||||
if not cache.has_key(lookup):
|
||||
cache[lookup] = fetch_genre(LASTFM.get_track(obj.artist, obj.title))
|
||||
return cache[lookup]
|
||||
|
||||
options = {
|
||||
'whitelist': None,
|
||||
'branches': None,
|
||||
'c14n': False,
|
||||
}
|
||||
# simple cache to speed up artist and album lookups track or album mode. it's
|
||||
# probably not required to cache track lookups, but...
|
||||
cache = {}
|
||||
class LastGenrePlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(LastGenrePlugin, self).__init__()
|
||||
|
|
@ -157,9 +192,9 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
'fallback': None,
|
||||
'canonical': None,
|
||||
'source': 'album',
|
||||
'force': False,
|
||||
})
|
||||
|
||||
|
||||
# Read the whitelist file.
|
||||
wl_filename = self.config['whitelist'].as_filename()
|
||||
whitelist = set()
|
||||
|
|
@ -184,62 +219,127 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
options['branches'] = branches
|
||||
options['c14n'] = True
|
||||
|
||||
def _set_sources(self, source):
|
||||
"""Prepare our internal represantation of valid sources we can use.
|
||||
"""
|
||||
self.sources = []
|
||||
if source == 'track':
|
||||
self.sources.extend(['track', 'album', 'artist'])
|
||||
elif source == 'album':
|
||||
self.sources.extend(['album', 'artist'])
|
||||
elif source == 'artist':
|
||||
self.sources.extend(['artist'])
|
||||
|
||||
def _get_album_genre(self, album, force, fallback_str):
|
||||
"""Return the best candidate for album genre based on sources (see
|
||||
_set_sources).
|
||||
Going down from album -> artist -> original -> fallback -> None.
|
||||
"""
|
||||
if not force and is_allowed(album.genre):
|
||||
return [album.genre, 'keep']
|
||||
result = None
|
||||
# no track lookup for album genre
|
||||
if 'album' in self.sources:
|
||||
result = fetch_album_genre(album)
|
||||
if result:
|
||||
return [result, 'album']
|
||||
if 'artist' in self.sources:
|
||||
# no artist lookup for Various Artists
|
||||
if not album.albumartist == 'Various Artists':
|
||||
result = fetch_album_artist_genre(album)
|
||||
if result:
|
||||
return [result, 'artist']
|
||||
if is_allowed(album.genre):
|
||||
return [album.genre, 'original']
|
||||
if fallback_str:
|
||||
return [fallback_str, 'fallback']
|
||||
return [None, None]
|
||||
|
||||
|
||||
def _get_item_genre(self, item, force, fallback_str):
|
||||
"""Return the best candidate for item genre based on sources (see
|
||||
_set_sources).
|
||||
Going down from track -> album -> artist -> original -> fallback ->
|
||||
None.
|
||||
"""
|
||||
if not force:
|
||||
if is_allowed(item.genre):
|
||||
return [item.genre, 'keep']
|
||||
result = None
|
||||
if 'track' in self.sources:
|
||||
result = fetch_track_genre(item)
|
||||
if result:
|
||||
return [result, 'track']
|
||||
if 'album' in self.sources:
|
||||
if item.album:
|
||||
result = fetch_album_genre(item)
|
||||
if result:
|
||||
return [result, 'album']
|
||||
if 'artist' in self.sources:
|
||||
result = fetch_artist_genre(item)
|
||||
if result:
|
||||
return [result, 'artist']
|
||||
if is_allowed(item.genre):
|
||||
return [item.genre, 'original']
|
||||
if fallback_str:
|
||||
return [fallback_str, 'fallback']
|
||||
return [None, None]
|
||||
|
||||
def commands(self):
|
||||
lastgenre_cmd = ui.Subcommand('lastgenre', help='fetch genres')
|
||||
lastgenre_cmd.parser.add_option('-f', '--force', dest='force',
|
||||
action='store_true',
|
||||
default=self.config['force'].get(bool),
|
||||
help='re-download genre when already present')
|
||||
lastgenre_cmd.parser.add_option('-v', '--verbose', dest='verbose',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='be more verbose')
|
||||
lastgenre_cmd.parser.add_option('-s', '--source', dest='source',
|
||||
type='string',
|
||||
default=self.config['source'].get(),
|
||||
help='set source, one of: artist / album / track')
|
||||
def lastgenre_func(lib, opts, args):
|
||||
# The "write to files" option corresponds to the
|
||||
# import_write config value.
|
||||
write = config['import']['write'].get(bool)
|
||||
force = opts.force
|
||||
self._set_sources(opts.source)
|
||||
fallback_str = self.config['fallback'].get()
|
||||
for album in lib.albums(ui.decargs(args)):
|
||||
tags = []
|
||||
lastfm_obj = _lastfm_obj(album)
|
||||
if album.genre:
|
||||
tags.append(album.genre)
|
||||
|
||||
tags.extend(_tags_for(lastfm_obj))
|
||||
genre = _tags_to_genre(tags)
|
||||
|
||||
fallback_str = self.config['fallback'].get()
|
||||
if not genre and fallback_str != None:
|
||||
genre = fallback_str
|
||||
log.debug(u'no last.fm genre found: fallback to %s' % genre)
|
||||
|
||||
if genre is not None:
|
||||
log.debug(u'adding last.fm album genre: %s' % genre)
|
||||
album.genre = genre
|
||||
album.genre, src = self._get_album_genre(album, force, fallback_str)
|
||||
if opts.verbose:
|
||||
log.info(u'LastGenre: Album({0} - {1}) > {2}({3})'.format(
|
||||
album.albumartist, album.album, album.genre, src))
|
||||
for item in album.items():
|
||||
item.genre, src = self._get_item_genre(item, force,
|
||||
fallback_str)
|
||||
lib.store(item)
|
||||
if opts.verbose:
|
||||
log.info(u'LastGenre: Item({0} - {1}) > {2}({3})'.format(
|
||||
item.artist, item.title, item.genre, src))
|
||||
if write:
|
||||
for item in album.items():
|
||||
item.write()
|
||||
item.write()
|
||||
|
||||
lastgenre_cmd.func = lastgenre_func
|
||||
return [lastgenre_cmd]
|
||||
|
||||
def imported(self, session, task):
|
||||
self._set_sources(self.config['source'].get())
|
||||
tags = []
|
||||
fallback_str = self.config['fallback'].get()
|
||||
if task.is_album:
|
||||
album = session.lib.get_album(task.album_id)
|
||||
lastfm_obj = _lastfm_obj(album)
|
||||
if album.genre:
|
||||
tags.append(album.genre)
|
||||
album.genre, src = self._get_album_genre(album, True, fallback_str)
|
||||
log.debug(u'added last.fm album genre ({0}): {1}'.format(
|
||||
src, album.genre))
|
||||
for item in album.items():
|
||||
item.genre, src = self._get_item_genre(item, True, fallback_str)
|
||||
log.debug(u'added last.fm item genre ({0}): {1}'.format(
|
||||
src, item.genre))
|
||||
else:
|
||||
item = task.item
|
||||
lastfm_obj = _lastfm_obj(item)
|
||||
if item.genre:
|
||||
tags.append(item.genre)
|
||||
|
||||
tags.extend(_tags_for(lastfm_obj))
|
||||
genre = _tags_to_genre(tags)
|
||||
|
||||
fallback_str = self.config['fallback'].get()
|
||||
if not genre and fallback_str != None:
|
||||
genre = fallback_str
|
||||
log.debug(u'no last.fm genre found: fallback to %s' % genre)
|
||||
|
||||
if genre is not None:
|
||||
log.debug(u'adding last.fm album genre: %s' % genre)
|
||||
|
||||
if task.is_album:
|
||||
album = session.lib.get_album(task.album_id)
|
||||
album.genre = genre
|
||||
else:
|
||||
item.genre = genre
|
||||
session.lib.store(item)
|
||||
item.genre, src = self._get_item_genre(item, True, fallback_str)
|
||||
log.debug(u'added last.fm item genre ({0}): {1}'.format(
|
||||
src, item.genre))
|
||||
session.lib.store(item)
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ to use your own tree.
|
|||
Genre Source
|
||||
------------
|
||||
|
||||
When looking up genres for albums or individual tracks, you may prefer to use a
|
||||
genre tag from the *artist* instead of the individual entity. To do so, set the
|
||||
``source`` configuration value to "artist", like so::
|
||||
When looking up genres for albums or individual tracks, you may prefer to use
|
||||
a genre tag from the *track* or *artist* instead of the individual entity. To
|
||||
do so, set the ``source`` configuration value to "track" or "artist", like so::
|
||||
|
||||
lastgenre:
|
||||
source: artist
|
||||
|
|
|
|||
Loading…
Reference in a new issue