diff --git a/beetsplug/missing.py b/beetsplug/missing.py new file mode 100644 index 000000000..0d59d22fb --- /dev/null +++ b/beetsplug/missing.py @@ -0,0 +1,150 @@ +# This file is 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_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`. + ''' + item_mbids = map(lambda x: x.mb_trackid, album.items()) + + 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) + 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 _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.config.add({'count': False}) + self.config.add({'total': False}) + + 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') + + 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() + + 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 item in _missing(album): + print_obj(item, lib, fmt=fmt) + + self._command.func = _miss + return [self._command] diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 3c9eb4d9f..443784966 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -61,7 +61,8 @@ disabled by default, but you can turn them on as described above. info smartplaylist mbsync - + missing + Autotagger Extensions '''''''''''''''''''''' @@ -110,7 +111,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..122ff51f2 --- /dev/null +++ b/docs/plugins/missing.rst @@ -0,0 +1,97 @@ +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 the following configuration directives, either in +your configuration file:: + + missing: + format: FMTSTR + count: bool + total: bool + +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 + -c, --count count missing tracks per album + -t, --total count total of missing tracks + + +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. + +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 + +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 + ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇ + +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