From 4df95210072d63aad5460dc1090804d6ba02f310 Mon Sep 17 00:00:00 2001 From: Peter Schnebel Date: Mon, 4 Feb 2013 20:24:22 +0100 Subject: [PATCH 1/6] experimental track / album / artist based genre stuff per item --- beets/library.py | 4 +- beetsplug/lastgenre/__init__.py | 219 ++++++++++++++++++++++---------- 2 files changed, 154 insertions(+), 69 deletions(-) diff --git a/beets/library.py b/beets/library.py index f4bfc467f..c0178c62b 100644 --- a/beets/library.py +++ b/beets/library.py @@ -117,7 +117,7 @@ ALBUM_FIELDS = [ ('albumartist_sort', 'text', True), ('albumartist_credit', 'text', True, True), ('album', 'text', True), - ('genre', 'text', True), + ('genre', 'text', False), ('year', 'int', True), ('month', 'int', True), ('day', 'int', True), @@ -259,6 +259,7 @@ class Item(object): self.record[key] = value self.dirty[key] = True if key in ITEM_KEYS_WRITABLE: + log.debug(u'setting item %s = %s', key, value) self.mtime = 0 # Reset mtime on dirty. else: super(Item, self).__setattr__(key, value) @@ -1451,6 +1452,7 @@ class Album(BaseAlbum): # Possibly make modification on items as well. if key in ALBUM_KEYS_ITEM: + log.debug('copying %s = %s to all items' % (key, value)) for item in self.items(): setattr(item, key, value) self._library.store(item) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 25c6afe5a..de5101c2b 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -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,78 @@ 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']: + log.debug(u'verfied genre: %s' % genre) + 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): + tags = [] + tags.extend(_tags_for(lastfm_obj)) + return _tags_to_genre(tags) + +def fetch_album_genre(obj): + lookup = u'{0}-{1}'.format(obj.albumartist, obj.album) + if cache['album'].has_key(lookup): + log.debug(u'using cache: %s = %s' % (lookup, cache['album'][lookup])) + return cache['album'][lookup] + cache['album'][lookup] = \ + fetch_genre(LASTFM.get_album(obj.albumartist, obj.album)) + log.debug(u'setting cache: %s = %s' % (lookup, cache['album'][lookup])) + return cache['album'][lookup] + +def fetch_album_artist_genre(obj): + lookup = obj.albumartist + if cache['artist'].has_key(lookup): + log.debug(u'using cache: %s = %s' % (lookup, cache['artist'][lookup])) + return cache['artist'][lookup] + cache['artist'][lookup] = \ + fetch_genre(LASTFM.get_artist(obj.albumartist)) + log.debug(u'setting cache: %s = %s' % (lookup, cache['artist'][lookup])) + return cache['artist'][lookup] + +def fetch_artist_genre(obj): + lookup = obj.artist + if cache['artist'].has_key(lookup): + log.debug(u'using cache: %s = %s' % (lookup, cache['artist'][lookup])) + return cache['artist'][lookup] + cache['artist'][lookup] = \ + fetch_genre(LASTFM.get_artist(obj.artist)) + log.debug(u'setting cache: %s = %s' % (lookup, cache['artist'][lookup])) + return cache['artist'][lookup] + +def fetch_track_genre(obj): + lookup = u'{0}-{1}'.format(obj.artist, obj.title) + if cache['track'].has_key(lookup): + log.debug(u'using cache: %s = %s' % (lookup, cache['track'][lookup])) + return cache['track'][lookup] + cache['track'][lookup] = \ + fetch_genre(LASTFM.get_track(obj.artist, obj.title)) + log.debug(u'setting cache: %s = %s' % (lookup, cache['track'][lookup])) + return cache['track'][lookup] + options = { 'whitelist': None, 'branches': None, 'c14n': False, } +sources = [] +cache = {'artist':{}, 'album':{}, 'track':{}} class LastGenrePlugin(plugins.BeetsPlugin): def __init__(self): super(LastGenrePlugin, self).__init__() @@ -157,9 +195,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() @@ -170,6 +208,15 @@ class LastGenrePlugin(plugins.BeetsPlugin): whitelist.add(line) options['whitelist'] = whitelist + # Prepare sources + source = self.config['source'].get() + if source == 'track': + sources.extend(['track', 'album', 'artist']) + elif source == 'album': + sources.extend(['album', 'artist']) + elif source == 'artist': + sources.extend(['artist']) + # Read the genres tree for canonicalization if enabled. c14n_filename = self.config['canonical'].get() if c14n_filename is not None: @@ -184,62 +231,98 @@ class LastGenrePlugin(plugins.BeetsPlugin): options['branches'] = branches options['c14n'] = True + def _get_album_genre(self, album, force, fallback_str): + log.debug(u'_get_album_genre') + if not force and is_allowed(album.genre): + # already valid and no forced lookup + log.debug(u"not fetching album genre. already valid") + return album.genre + result = None + # no track lookup for album + if 'album' in sources: + result = fetch_album_genre(album) + log.debug(u"last.fm album genre: %s" % result) + if result: + return result + if 'artist' in sources: + if not album.albumartist == 'Various Artists': + # no artist lookup for Various Artists + result = fetch_album_artist_genre(album) + log.debug(u"last.fm album artist genre: %s" % result) + if result: + return result + if is_allowed(album.genre): + return album.genre + if fallback_str: + return fallback_str + return None + + + def _get_item_genre(self, item, force, fallback_str): + if not force: + if is_allowed(item.genre): + # already valid and no forced lookup + log.debug(u"not fetching item genre. already valid") + return item.genre + log.debug(u"replacing invalid item genre: %s" % item.genre) + result = None + if 'track' in sources: + result = fetch_track_genre(item) + if result: + return result + log.debug(u"no last.fm track genre") + if 'album' in sources: + if item.album: + result = fetch_album_genre(item) + if result: + return result + log.debug(u"no last.fm album genre") + if 'artist' in sources: + result = fetch_artist_genre(item) + if result: + return result + log.debug(u"no last.fm artist genre") + if is_allowed(item.genre): + return item.genre + if fallback_str: + return fallback_str + return result + def commands(self): lastgenre_cmd = ui.Subcommand('lastgenre', help='fetch genres') 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 = self.config['force'].get(bool) + 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 = self._get_album_genre(album, force, fallback_str) + log.debug(u'adding last.fm album genre: %s' % album.genre) + for item in album.items(): + item.genre = self._get_item_genre(item, force, + fallback_str) + log.debug(u'adding last.fm item genre: %s' % item.genre) if write: - for item in album.items(): - item.write() + item.write() + lastgenre_cmd.func = lastgenre_func return [lastgenre_cmd] def imported(self, session, task): tags = [] - if task.is_album: - album = session.lib.get_album(task.album_id) - lastfm_obj = _lastfm_obj(album) - if album.genre: - tags.append(album.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) + if task.is_album: + log.debug(u'imported: album') + album = session.lib.get_album(task.album_id) + album.genre = self._get_album_genre(album, True, fallback_str) + log.debug(u'adding last.fm album genre: %s' % album.genre) + for item in album.items(): + item.genre = self._get_item_genre(item, True, fallback_str) + log.debug(u'adding last.fm item genre: %s' % item.genre) + else: + log.debug(u'imported: item') + item = task.item + item.genre = self._get_item_genre(item, True, fallback_str) + log.debug(u'adding last.fm item genre: %s' % item.genre) + session.lib.store(item) From bd7303459e1e816baca4dfc9eb98e612a976d1e1 Mon Sep 17 00:00:00 2001 From: Peter Schnebel Date: Mon, 4 Feb 2013 21:47:09 +0100 Subject: [PATCH 2/6] removed debug messages (i should really use git more) --- beets/library.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/beets/library.py b/beets/library.py index c0178c62b..cbce23688 100644 --- a/beets/library.py +++ b/beets/library.py @@ -259,7 +259,6 @@ class Item(object): self.record[key] = value self.dirty[key] = True if key in ITEM_KEYS_WRITABLE: - log.debug(u'setting item %s = %s', key, value) self.mtime = 0 # Reset mtime on dirty. else: super(Item, self).__setattr__(key, value) @@ -1452,11 +1451,9 @@ class Album(BaseAlbum): # Possibly make modification on items as well. if key in ALBUM_KEYS_ITEM: - log.debug('copying %s = %s to all items' % (key, value)) for item in self.items(): setattr(item, key, value) self._library.store(item) - else: object.__setattr__(self, key, value) From 30dff5afffee4cc2417e5d8b5253976d90d4132e Mon Sep 17 00:00:00 2001 From: Peter Schnebel Date: Mon, 4 Feb 2013 21:47:35 +0100 Subject: [PATCH 3/6] cleaned up, removed debug --- beetsplug/lastgenre/__init__.py | 200 +++++++++++++++++--------------- 1 file changed, 108 insertions(+), 92 deletions(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index de5101c2b..1334ebbab 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -120,7 +120,6 @@ def is_allowed(genre): if genre is None: return False if genre.lower() in options['whitelist']: - log.debug(u'verfied genre: %s' % genre) return True return False @@ -134,57 +133,55 @@ def find_allowed(genres): return None def fetch_genre(lastfm_obj): - tags = [] - tags.extend(_tags_for(lastfm_obj)) - return _tags_to_genre(tags) + """Returns the genre for this lastfm_obj. + """ + return _tags_to_genre(_tags_for(lastfm_obj)) def fetch_album_genre(obj): - lookup = u'{0}-{1}'.format(obj.albumartist, obj.album) - if cache['album'].has_key(lookup): - log.debug(u'using cache: %s = %s' % (lookup, cache['album'][lookup])) - return cache['album'][lookup] - cache['album'][lookup] = \ - fetch_genre(LASTFM.get_album(obj.albumartist, obj.album)) - log.debug(u'setting cache: %s = %s' % (lookup, cache['album'][lookup])) - return cache['album'][lookup] + """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): - lookup = obj.albumartist - if cache['artist'].has_key(lookup): - log.debug(u'using cache: %s = %s' % (lookup, cache['artist'][lookup])) - return cache['artist'][lookup] - cache['artist'][lookup] = \ - fetch_genre(LASTFM.get_artist(obj.albumartist)) - log.debug(u'setting cache: %s = %s' % (lookup, cache['artist'][lookup])) - return cache['artist'][lookup] + """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): - lookup = obj.artist - if cache['artist'].has_key(lookup): - log.debug(u'using cache: %s = %s' % (lookup, cache['artist'][lookup])) - return cache['artist'][lookup] - cache['artist'][lookup] = \ - fetch_genre(LASTFM.get_artist(obj.artist)) - log.debug(u'setting cache: %s = %s' % (lookup, cache['artist'][lookup])) - return cache['artist'][lookup] + """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): - lookup = u'{0}-{1}'.format(obj.artist, obj.title) - if cache['track'].has_key(lookup): - log.debug(u'using cache: %s = %s' % (lookup, cache['track'][lookup])) - return cache['track'][lookup] - cache['track'][lookup] = \ - fetch_genre(LASTFM.get_track(obj.artist, obj.title)) - log.debug(u'setting cache: %s = %s' % (lookup, cache['track'][lookup])) - return cache['track'][lookup] + """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, } -sources = [] -cache = {'artist':{}, 'album':{}, 'track':{}} +# 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__() @@ -208,15 +205,6 @@ class LastGenrePlugin(plugins.BeetsPlugin): whitelist.add(line) options['whitelist'] = whitelist - # Prepare sources - source = self.config['source'].get() - if source == 'track': - sources.extend(['track', 'album', 'artist']) - elif source == 'album': - sources.extend(['album', 'artist']) - elif source == 'artist': - sources.extend(['artist']) - # Read the genres tree for canonicalization if enabled. c14n_filename = self.config['canonical'].get() if c14n_filename is not None: @@ -231,78 +219,104 @@ 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): - log.debug(u'_get_album_genre') + """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): - # already valid and no forced lookup - log.debug(u"not fetching album genre. already valid") - return album.genre + return [album.genre, 'keep'] result = None - # no track lookup for album - if 'album' in sources: + # no track lookup for album genre + if 'album' in self.sources: result = fetch_album_genre(album) - log.debug(u"last.fm album genre: %s" % result) if result: - return result - if 'artist' in sources: + return [result, 'album'] + if 'artist' in self.sources: + # no artist lookup for Various Artists if not album.albumartist == 'Various Artists': - # no artist lookup for Various Artists result = fetch_album_artist_genre(album) - log.debug(u"last.fm album artist genre: %s" % result) if result: - return result + return [result, 'artist'] if is_allowed(album.genre): - return album.genre + return [album.genre, 'original'] if fallback_str: - return fallback_str - return None + 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): - # already valid and no forced lookup - log.debug(u"not fetching item genre. already valid") - return item.genre - log.debug(u"replacing invalid item genre: %s" % item.genre) + return [item.genre, 'keep'] result = None - if 'track' in sources: + if 'track' in self.sources: result = fetch_track_genre(item) if result: - return result - log.debug(u"no last.fm track genre") - if 'album' in sources: + return [result, 'track'] + if 'album' in self.sources: if item.album: result = fetch_album_genre(item) if result: - return result - log.debug(u"no last.fm album genre") - if 'artist' in sources: + return [result, 'album'] + if 'artist' in self.sources: result = fetch_artist_genre(item) if result: - return result - log.debug(u"no last.fm artist genre") + return [result, 'artist'] if is_allowed(item.genre): - return item.genre + return [item.genre, 'original'] if fallback_str: - return fallback_str - return result + 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 = self.config['force'].get(bool) - fallback_str = self.config['fallback'].get() + force = opts.force + self._set_sources(opts.source) for album in lib.albums(ui.decargs(args)): - album.genre = self._get_album_genre(album, force, fallback_str) - log.debug(u'adding last.fm album genre: %s' % album.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 = self._get_item_genre(item, force, + item.genre, src = self._get_item_genre(item, force, fallback_str) - log.debug(u'adding last.fm item genre: %s' % item.genre) + 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: item.write() @@ -310,19 +324,21 @@ class LastGenrePlugin(plugins.BeetsPlugin): 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: - log.debug(u'imported: album') album = session.lib.get_album(task.album_id) - album.genre = self._get_album_genre(album, True, fallback_str) - log.debug(u'adding last.fm album genre: %s' % 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 = self._get_item_genre(item, True, fallback_str) - log.debug(u'adding last.fm item genre: %s' % item.genre) + 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: - log.debug(u'imported: item') item = task.item - item.genre = self._get_item_genre(item, True, fallback_str) - log.debug(u'adding last.fm item genre: %s' % item.genre) + 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) From 936fdc303d4c86da1734805fc9ccdc8729072112 Mon Sep 17 00:00:00 2001 From: Peter Schnebel Date: Mon, 4 Feb 2013 22:39:20 +0100 Subject: [PATCH 4/6] Added track to source options in the docs. --- docs/plugins/lastgenre.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/lastgenre.rst b/docs/plugins/lastgenre.rst index cdd3dfd82..59c51db3e 100644 --- a/docs/plugins/lastgenre.rst +++ b/docs/plugins/lastgenre.rst @@ -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 From f83e9fb8bbbb1ebbc62e3b8e0c61c628caa55ec4 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 5 Feb 2013 08:13:36 +0100 Subject: [PATCH 5/6] qbugfix --- beetsplug/lastgenre/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 1334ebbab..0bbf633e3 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -305,6 +305,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): 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)): album.genre, src = self._get_album_genre(album, force, fallback_str) if opts.verbose: From e730e18b628366617393d5d44e13318eb92d414d Mon Sep 17 00:00:00 2001 From: root Date: Tue, 5 Feb 2013 08:30:10 +0100 Subject: [PATCH 6/6] reverted de-couling of album.genre and item.genre --- beets/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/library.py b/beets/library.py index cbce23688..3146d7ba9 100644 --- a/beets/library.py +++ b/beets/library.py @@ -117,7 +117,7 @@ ALBUM_FIELDS = [ ('albumartist_sort', 'text', True), ('albumartist_credit', 'text', True, True), ('album', 'text', True), - ('genre', 'text', False), + ('genre', 'text', True), ('year', 'int', True), ('month', 'int', True), ('day', 'int', True),