From 6ae73bc9217d03260c468368ccbb75e997151a53 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 14:13:53 +0100 Subject: [PATCH 01/10] Initial import of missing tracks plugin This plugin adds a new command, ``missing`` or ``miss``, which finds and lists, for every album in your collection, which tracks are missing. --- beetsplug/missing.py | 132 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 beetsplug/missing.py diff --git a/beetsplug/missing.py b/beetsplug/missing.py new file mode 100644 index 000000000..0d3005b1d --- /dev/null +++ b/beetsplug/missing.py @@ -0,0 +1,132 @@ +# This file is not part of beets. +# Copyright 2013, Pedro Silva. +# +# 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. + +'''list missing tracks +''' +import logging + +from beets.autotag import hooks +from beets.library import Item +from beets.plugins import BeetsPlugin +from beets.ui import decargs, print_obj, Subcommand + +plugin = 'missing' +log = logging.getLogger('beets') + + +def _missing(album, lib=None): + '''Query MusicBrainz to determine items missing from `album`, + caching them in `lib` to avoid further queries. + ''' + item_paths = filter(None, map(lambda i: i.path, album.items())) + item_mbids = map(lambda x: x.mb_trackid, + filter(lambda i: i.path is not None, album.items())) + + if len(item_paths) < album.tracktotal: + # fetch missing items + # TODO: Implement caching that without breaking other stuff + album_info = hooks._album_for_id(album.mb_albumid) + for track_info in getattr(album_info, 'tracks', []): + if track_info.track_id not in item_mbids: + item = _item(track_info, album_info, album.id) + log.debug('{}: track {} in album {}' + .format(plugin, + track_info.track_id, + album_info.album_id)) + yield item + + +def _format(song): + ''' + Format unicode-encoded representation of a pygrooveshark song. + ''' + return " - ".join([song.artist.name.encode('utf-8'), + song.album.name.encode('utf-8'), + song.name.encode('utf-8')]) + + +def _item(track_info, album_info, album_id): + '''Build and return `item` from `track_info` and `album info` + objects. `item` is missing what fields cannot be obtained from + MusicBrainz alone (encoder, rg_track_gain, rg_track_peak, + rg_album_gain, rg_album_peak, original_year, original_month, + original_day, length, bitrate, format, samplerate, bitdepth, + channels, mtime.) + ''' + t = track_info + a = album_info + + return Item({'album_id': album_id, + 'album': a.album, + 'albumartist': a.artist, + 'albumartist_credit': a.artist_credit, + 'albumartist_sort': a.artist_sort, + 'albumdisambig': a.albumdisambig, + 'albumstatus': a.albumstatus, + 'albumtype': a.albumtype, + 'artist': t.artist, + 'artist_credit': t.artist_credit, + 'artist_sort': t.artist_sort, + 'asin': a.asin, + 'catalognum': a.catalognum, + 'comp': a.va, + 'country': a.country, + 'day': a.day, + 'disc': t.medium, + 'disctitle': t.disctitle, + 'disctotal': a.mediums, + 'label': a.label, + 'language': a.language, + 'length': t.length, + 'mb_albumid': a.album_id, + 'mb_artistid': t.artist_id, + 'mb_releasegroupid': a.releasegroup_id, + 'mb_trackid': t.track_id, + 'media': a.media, + 'month': a.month, + 'script': a.script, + 'title': t.title, + 'track': t.index, + 'tracktotal': len(a.tracks), + 'year': a.year}) + + +class MissingPlugin(BeetsPlugin): + '''List missing tracks + ''' + def __init__(self): + super(MissingPlugin, self).__init__() + + self.config.add({'format': None}) + + self._command = Subcommand('missing', + help=__doc__, + aliases=['miss']) + + self._command.parser.add_option('-f', '--format', dest='format', + action='store', type='string', + help='print with custom FORMAT', + metavar='FORMAT') + + def commands(self): + def _miss(lib, opts, args): + self.config.set_args(opts) + fmt = self.config['format'].get() + + for album in lib.albums(decargs(args)): + for item in _missing(album, lib): + print_obj(item, lib, fmt=fmt) + + self._command.func = _miss + return [self._command] From 6ac86f9f4a367515242fc9c72bc686787703f07e Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 14:14:58 +0100 Subject: [PATCH 02/10] Add documentation for missing tracks plugin Also add link to the documentation plugin to the index --- docs/plugins/index.rst | 6 ++-- docs/plugins/missing.rst | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 docs/plugins/missing.rst diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 2217f4d91..909e5dead 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -60,7 +60,8 @@ disabled by default, but you can turn them on as described above. convert info smartplaylist - + missing + Autotagger Extensions '''''''''''''''''''''' @@ -108,7 +109,8 @@ Miscellaneous * :doc:`convert`: Transcode music and embed album art while exporting to a different directory. * :doc:`info`: Print music files' tags to the console. - +* :doc:`missing`: List missing tracks. + .. _MPD: http://mpd.wikia.com/ .. _MPD clients: http://mpd.wikia.com/wiki/Clients diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst new file mode 100644 index 000000000..66af85b72 --- /dev/null +++ b/docs/plugins/missing.rst @@ -0,0 +1,71 @@ +Missing Plugin +============== + +This plugin adds a new command, ``missing`` or ``miss``, which finds +and lists, for every album in your collection, which tracks are +missing. Listing missing files requires one network call to +MusicBrainz. + +Installation +------------ + +Enable the plugin by putting ``missing`` on your ``plugins`` line in +:doc:`config file `:: + + plugins: + missing + ... + +Configuration +------------- + +The plugin accepts a single configuration directive, ``format``, +either in your configuration file:: + + missing: + format: FMTSTR + +or in the command-line:: + + $ beet missing --help + Usage: beet missing [options] + + Options: + -h, --help show this help message and exit + -f FORMAT, --format=FORMAT + print with custom FORMAT + +format +~~~~~~ + +The ``format`` option (default: ``None``) lets you specify a specific +format with which to print every track. This uses the same template +syntax as beets’ :doc:`path formats `. The usage +is inspired by, and therefore similar to, the :ref:`list ` +command. + +Examples +------------------------- + +List all missing tracks in your collection:: + + beet missing + +List all missing tracks from 2008:: + + beet missing year:2008 + +Print out a unicode histogram of the missing track years using `spark`_:: + + beet missing -f '$year' | spark + ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇ + + +TODO +---- + +- Add caching. + +-------------- + +.. _spark: https://github.com/holman/spark From 18e2704ffbbb702753c2250a609b8bd06737f127 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 14:23:32 +0100 Subject: [PATCH 03/10] Remove unnecessary _format function --- beetsplug/missing.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 0d3005b1d..e61cf6c0b 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -47,15 +47,6 @@ def _missing(album, lib=None): yield item -def _format(song): - ''' - Format unicode-encoded representation of a pygrooveshark song. - ''' - return " - ".join([song.artist.name.encode('utf-8'), - song.album.name.encode('utf-8'), - song.name.encode('utf-8')]) - - def _item(track_info, album_info, album_id): '''Build and return `item` from `track_info` and `album info` objects. `item` is missing what fields cannot be obtained from From abe12be4b2fcb2434cb9feff3b2b0a5597f50a4d Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:06:30 +0100 Subject: [PATCH 04/10] Remove unneeded Library argument from _missing --- beetsplug/missing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index e61cf6c0b..f46f7906a 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -25,9 +25,8 @@ plugin = 'missing' log = logging.getLogger('beets') -def _missing(album, lib=None): - '''Query MusicBrainz to determine items missing from `album`, - caching them in `lib` to avoid further queries. +def _missing(album): + '''Query MusicBrainz to determine items missing from `album`. ''' item_paths = filter(None, map(lambda i: i.path, album.items())) item_mbids = map(lambda x: x.mb_trackid, @@ -116,7 +115,7 @@ class MissingPlugin(BeetsPlugin): fmt = self.config['format'].get() for album in lib.albums(decargs(args)): - for item in _missing(album, lib): + for item in _missing(album): print_obj(item, lib, fmt=fmt) self._command.func = _miss From bed7c5ae880f3219d204aa94f5597fd8b391ab00 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:41:04 +0100 Subject: [PATCH 05/10] Add _missing_count function --- beetsplug/missing.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index f46f7906a..74b1b04aa 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -25,6 +25,12 @@ plugin = 'missing' log = logging.getLogger('beets') +def _missing_count(album): + '''Return number of missing items in `album`. + ''' + return album.tracktotal - len([i for i in album.items()]) + + def _missing(album): '''Query MusicBrainz to determine items missing from `album`. ''' From 69877f201355c38d98ea625018a41f7d41b612c0 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:42:10 +0100 Subject: [PATCH 06/10] Add 'count' and 'total' parameters - count: count missing tracks per album - total: count total missing tracks --- beetsplug/missing.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 74b1b04aa..dc2e77277 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -105,6 +105,8 @@ class MissingPlugin(BeetsPlugin): super(MissingPlugin, self).__init__() self.config.add({'format': None}) + self.config.add({'count': False}) + self.config.add({'total': False}) self._command = Subcommand('missing', help=__doc__, @@ -115,10 +117,21 @@ class MissingPlugin(BeetsPlugin): help='print with custom FORMAT', metavar='FORMAT') + self._command.parser.add_option('-c', '--count', dest='count', + action='store_true', + help='count missing tracks per album') + + self._command.parser.add_option('-t', '--total', dest='total', + action='store_true', + help='count total of missing tracks') + def commands(self): def _miss(lib, opts, args): self.config.set_args(opts) fmt = self.config['format'].get() + count = self.config['count'].get() + total = self.config['total'].get() + for album in lib.albums(decargs(args)): for item in _missing(album): From d32a3ef9b56fa092421a3be6e0d53c316920af04 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:42:45 +0100 Subject: [PATCH 07/10] Implement logic for displaying missing track counts --- beetsplug/missing.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index dc2e77277..4d7bfbc2d 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -132,8 +132,19 @@ class MissingPlugin(BeetsPlugin): count = self.config['count'].get() total = self.config['total'].get() + albums = lib.albums(decargs(args)) + if total: + print(sum([_missing_count(a) for a in albums])) + return + + for album in albums: + if count: + missing = _missing_count(album) + if missing: + fmt = "$album: {}".format(missing) + print_obj(album, lib, fmt=fmt) + continue - for album in lib.albums(decargs(args)): for item in _missing(album): print_obj(item, lib, fmt=fmt) From 763cfa06b47eea3d7eb97f4addca5cc6625c2376 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:43:17 +0100 Subject: [PATCH 08/10] Don't check for item.paths == None as that can't happen --- beetsplug/missing.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 4d7bfbc2d..54fa2a846 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -34,11 +34,9 @@ def _missing_count(album): def _missing(album): '''Query MusicBrainz to determine items missing from `album`. ''' - item_paths = filter(None, map(lambda i: i.path, album.items())) - item_mbids = map(lambda x: x.mb_trackid, - filter(lambda i: i.path is not None, album.items())) + item_mbids = map(lambda x: x.mb_trackid, album.items()) - if len(item_paths) < album.tracktotal: + if len([i for i in album.items()]) < album.tracktotal: # fetch missing items # TODO: Implement caching that without breaking other stuff album_info = hooks._album_for_id(album.mb_albumid) From 99355e60d4abaa588a1543278649a9ac5ccb0882 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:43:36 +0100 Subject: [PATCH 09/10] Synchronize documentation with new parameters and respective behavior --- docs/plugins/missing.rst | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 66af85b72..122ff51f2 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -19,11 +19,13 @@ Enable the plugin by putting ``missing`` on your ``plugins`` line in Configuration ------------- -The plugin accepts a single configuration directive, ``format``, -either in your configuration file:: +The plugin accepts the following configuration directives, either in +your configuration file:: missing: format: FMTSTR + count: bool + total: bool or in the command-line:: @@ -34,6 +36,9 @@ or in the command-line:: -h, --help show this help message and exit -f FORMAT, --format=FORMAT print with custom FORMAT + -c, --count count missing tracks per album + -t, --total count total of missing tracks + format ~~~~~~ @@ -44,28 +49,49 @@ syntax as beets’ :doc:`path formats `. The usage is inspired by, and therefore similar to, the :ref:`list ` command. +count +~~~~~ + +The ``count` option (default: ``False``) prints a count of missing +tracks per album, with ``format`` hard-coded to ``'$album: $count'``. + +total +~~~~~ + +The ``total`` option (default: ``False``) prints a single +count of missing tracks in all albums + + Examples ------------------------- List all missing tracks in your collection:: - beet missing + beet missing List all missing tracks from 2008:: - beet missing year:2008 + beet missing year:2008 Print out a unicode histogram of the missing track years using `spark`_:: - beet missing -f '$year' | spark - ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇ + beet missing -f '$year' | spark + ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇ + +Print out a listing of all albums with missing tracks, and respective counts:: + + beet missing -c + +Print out a count of the total number of missing tracks:: + + beet missing -t TODO ---- - Add caching. - + -------------- .. _spark: https://github.com/holman/spark From b2d8f04f05350e5290a168f8acc0112a3f9c2be4 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 10 May 2013 16:50:34 +0100 Subject: [PATCH 10/10] Modify "not part of beets" to "part of beets" --- beetsplug/missing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 54fa2a846..0d59d22fb 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -1,4 +1,4 @@ -# This file is not part of beets. +# This file is part of beets. # Copyright 2013, Pedro Silva. # # Permission is hereby granted, free of charge, to any person obtaining