From bcae495c506ca7f9eb09aed5f00c24de2434b495 Mon Sep 17 00:00:00 2001 From: Georges Dubus Date: Fri, 3 May 2013 18:19:44 +0200 Subject: [PATCH 01/23] Added a -e option to random that makes the distribution even among artists --- beetsplug/random.py | 14 ++++++++++++++ docs/plugins/random.rst | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/beetsplug/random.py b/beetsplug/random.py index dbf1c0b73..d543cfd1b 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -19,6 +19,8 @@ from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_obj from beets.util.functemplate import Template import random +from operator import attrgetter +from itertools import groupby def random_item(lib, opts, args): query = decargs(args) @@ -32,9 +34,19 @@ def random_item(lib, opts, args): objs = list(lib.albums(query=query)) else: objs = list(lib.items(query=query)) + if opts.equal_chance: + key = attrgetter('albumartist') + objs.sort(key=key) + # Now the objs are list of albums or items + objs = [list(v) for k, v in groupby(objs, key)] + number = min(len(objs), opts.number) objs = random.sample(objs, number) + if opts.equal_chance: + # Select one random item from that artist + objs = map(random.choice, objs) + for item in objs: print_obj(item, lib, template) @@ -48,6 +60,8 @@ random_cmd.parser.add_option('-f', '--format', action='store', help='print with custom format', default=None) random_cmd.parser.add_option('-n', '--number', action='store', type="int", help='number of objects to choose', default=1) +random_cmd.parser.add_option('-e', '--equal-chance', action='store_true', + help='each artist has the same chance') random_cmd.func = random_item class Random(BeetsPlugin): diff --git a/docs/plugins/random.rst b/docs/plugins/random.rst index a7fac2579..ef8737b52 100644 --- a/docs/plugins/random.rst +++ b/docs/plugins/random.rst @@ -16,6 +16,10 @@ command (see :doc:`/reference/cli`). To choose an album instead of a single track, use ``-a``; to print paths to items instead of metadata, use ``-p``; and to use a custom format for printing, use ``-f FORMAT``. +If the ``-e`` option is passed, the random choice will be even among +artists (the albumartist field) This makes sure that your antholofgy +of Bob Dylan won't make you listen to Bob Dylan 50% of the time. + The ``-n NUMBER`` option controls the number of objects that are selected and printed (default 1). To select 5 tracks from your library, type ``beet random -n5``. From 106ad99556c808468944e6563e0101cf9fdda9e1 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 15 May 2013 12:58:52 +0100 Subject: [PATCH 02/23] Perform template field substitution on albums - adds another traversal through all plugins' template_fields for each 'evaluate_template' call. - requires the following idiom (or equivalent): @Plugin.template_field(field') def _tmpl_field(album): """Return stuff. """ if isinstance(album, Album): return stuff --- beets/library.py | 4 ++++ docs/plugins/writing.rst | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/beets/library.py b/beets/library.py index 54b2c68e4..8d9060ad2 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1754,6 +1754,10 @@ class Album(BaseAlbum): mapping['artpath'] = displayable_path(mapping['artpath']) mapping['path'] = displayable_path(self.item_dir()) + # Get values from plugins. + for key, value in plugins.template_values(self).iteritems(): + mapping[key] = value + # Get template functions. funcs = DefaultTemplateFunctions().functions() funcs.update(plugins.template_funcs()) diff --git a/docs/plugins/writing.rst b/docs/plugins/writing.rst index d92ea8374..53f0a0656 100644 --- a/docs/plugins/writing.rst +++ b/docs/plugins/writing.rst @@ -261,6 +261,17 @@ that adds a ``$disc_and_track`` field:: With this plugin enabled, templates can reference ``$disc_and_track`` as they can any standard metadata field. +Note that the above idiom expects the argument ``item`` to be an +actual *track* item. If you'd like to provide a template field for +*albums*, you'll need to check the argument:: + + @MyPlugin.template_field('field') + def _tmpl_field(album): + """Return stuff. + """ + if isinstance(album, beets.library.Album): + return 'stuff' + Extend MediaFile ^^^^^^^^^^^^^^^^ From 4f5faeccaccc6d9189a91cf863d9da4ff5f77b2a Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 15 May 2013 13:41:54 +0100 Subject: [PATCH 03/23] Append plugin template fields to 'beet fields' output with -p, --plugins --- beets/ui/commands.py | 9 +++++++++ docs/reference/cli.rst | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 0e1a3e298..75fdd5d23 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -91,11 +91,20 @@ def _showdiff(field, oldval, newval): fields_cmd = ui.Subcommand('fields', help='show fields available for queries and format strings') +fields_cmd.parser.add_option('-p', '--plugins', dest='plugins', + action='store_true', + help='show available plugin fields as well') + def fields_func(lib, opts, args): print("Available item fields:") print(" " + "\n ".join([key for key in library.ITEM_KEYS])) print("\nAvailable album fields:") print(" " + "\n ".join([key for key in library.ALBUM_KEYS])) + if opts.plugins: + print("\nAvailable plugin fields:") + for plugin in plugins.find_plugins(): + fields = plugin.template_fields.iteritems() + print(" " + "\n ".join([key for (key, value) in fields])) fields_cmd.func = fields_func default_commands.append(fields_cmd) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 575c92968..a5e0438ab 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -250,11 +250,13 @@ fields `````` :: - beet fields + beet fields [-p] Show the item and album metadata fields available for use in :doc:`query` and :doc:`pathformat`. +The ``-p`` (``--plugins``) option shows available plugin fields in +addition to the standard ones. Global Flags ------------ From a48fde9f2c613b925c3dd6e8c4d951a1077ff39d Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 15 May 2013 15:09:00 +0100 Subject: [PATCH 04/23] Use newly defined '$missing' template field instead of hardcoded format (-c) --- beetsplug/missing.py | 17 ++++++++++++++--- docs/plugins/missing.rst | 21 ++++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index ac394d8de..ac1561648 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -17,7 +17,7 @@ import logging from beets.autotag import hooks -from beets.library import Item +from beets.library import Item, Album from beets.plugins import BeetsPlugin from beets.ui import decargs, print_obj, Subcommand @@ -139,8 +139,9 @@ class MissingPlugin(BeetsPlugin): if count: missing = _missing_count(album) if missing: - fmt = "$album: {}".format(missing) - print_obj(album, lib, fmt=fmt) + if not fmt: + fmt = '$albumartist - $album: $missing' + print_obj(album, lib, fmt=fmt.format(missing)) continue for item in _missing(album): @@ -148,3 +149,13 @@ class MissingPlugin(BeetsPlugin): self._command.func = _miss return [self._command] + + +@MissingPlugin.template_field('missing') +def _tmpl_missing(album): + """Return number of missing items in 'album'. + """ + if isinstance(album, Album): + return _missing_count(album) + else: + return '' diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 8789d143f..2418333d5 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -2,9 +2,10 @@ 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. +and lists, for every album in your collection, which or how many +tracks are missing. Listing missing files requires one network call to +MusicBrainz. Merely counting missing files avoids any network calls. + Installation ------------ @@ -49,8 +50,9 @@ 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'``. +The ``count`` option (default: false) prints a count of missing tracks +per album, with ``format`` defaulting to ``$albumartist - $album: +$missing``. total ~~~~~ @@ -58,6 +60,12 @@ total The ``total`` option (default: false) prints a single count of missing tracks in all albums +Metadata fields +--------------- + +With this plugin enabled, templates can reference ``$missing`` as they +can any standard metadata field for use in :doc:`Queries +` and :doc:`Path Formats `. Examples -------- @@ -83,6 +91,9 @@ Print out a count of the total number of missing tracks:: beet missing -t +Call this plugin from other beet commands:: + + beet ls -a -f '$albumartist - $album: $missing' TODO ---- From 7031c15418a31be44cafaaef1370d06f7c426a2b Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 17 May 2013 12:47:34 +0100 Subject: [PATCH 05/23] Initial import of 'duplicates' plugin --- beetsplug/duplicates.py | 109 ++++++++++++++++++++++++++++++++++++ docs/changelog.rst | 2 + docs/plugins/duplicates.rst | 107 +++++++++++++++++++++++++++++++++++ docs/plugins/index.rst | 8 ++- 4 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 beetsplug/duplicates.py create mode 100644 docs/plugins/duplicates.rst diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py new file mode 100644 index 000000000..9b037b3ed --- /dev/null +++ b/beetsplug/duplicates.py @@ -0,0 +1,109 @@ +# 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 duplicate tracks or albums. +""" +import logging + +from beets.plugins import BeetsPlugin +from beets.ui import decargs, print_obj, Subcommand + +PLUGIN = 'duplicates' +log = logging.getLogger('beets') + + +def _counts(items): + """Return count of ITEMS indexed by item_id. + """ + import collections + counts = collections.defaultdict(list) + for item in items: + item_id = getattr(item, 'mb_trackid', item.mb_albumid) + counts[item_id].append(item) + return counts + + +def _duplicates(items, full): + """Return duplicate ITEMS. + """ + counts = _counts(items) + offset = 0 if full else 1 + for item_id, items in counts.iteritems(): + if len(items) > 1: + yield (item_id, len(items)-offset, items[offset:]) + + +class DuplicatesPlugin(BeetsPlugin): + """List duplicate tracks or albums + """ + def __init__(self): + super(DuplicatesPlugin, self).__init__() + + self.config.add({'format': ''}) + self.config.add({'count': False}) + self.config.add({'album': False}) + self.config.add({'full': False}) + + self._command = Subcommand('duplicates', + help=__doc__, + aliases=['dup']) + + 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 duplicate tracks or\ + albums') + + self._command.parser.add_option('-a', '--album', dest='album', + action='store_true', + help='show duplicate albums instead\ + of tracks') + + self._command.parser.add_option('-F', '--full', dest='full', + action='store_true', + help='show all versions of duplicate\ + tracks or albums') + + def commands(self): + def _dup(lib, opts, args): + self.config.set_args(opts) + fmt = self.config['format'].get() + count = self.config['count'].get() + album = self.config['album'].get() + full = self.config['full'].get() + + if album: + items = lib.albums(decargs(args)) + else: + items = lib.items(decargs(args)) + + orig_fmt = fmt + for obj_id, obj_count, objs in _duplicates(items, full): + if count: + if not fmt: + if album: + fmt = '$albumartist - $album' + else: + fmt = '$albumartist - $album - $title' + fmt += ': {}' + for o in objs: + print_obj(o, lib, fmt=fmt.format(obj_count)) + fmt = orig_fmt + + self._command.func = _dup + return [self._command] diff --git a/docs/changelog.rst b/docs/changelog.rst index 885c2a7c6..061a5260d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,8 @@ Changelog 1.1.1 (in development) ---------------------- +* New :doc:`/plugins/duplicates`: Find tracks or albums in your + library that are **duplicated**. * New :doc:`/plugins/missing`: Find albums in your library that are **missing tracks**. Thanks to Pedro Silva. * Your library now keeps track of **when music was added** to it. The new diff --git a/docs/plugins/duplicates.rst b/docs/plugins/duplicates.rst new file mode 100644 index 000000000..f067896ec --- /dev/null +++ b/docs/plugins/duplicates.rst @@ -0,0 +1,107 @@ +Duplicates Plugin +============== + +This plugin adds a new command, ``duplicates`` or ``dup``, which finds +and lists duplicate tracks or albums in your collection. + +Installation +------------ + +Enable the plugin by putting ``duplicates`` on your ``plugins`` line in +:doc:`config file `:: + + plugins: + duplicates + ... + +Configuration +------------- + +By default, the ``beet duplicates`` command lists the names of tracks +in your library that are duplicates. It assumes that Musicbrainz track +and album ids are unique to each track or album. That is, it lists +every track or album with an ID that has been seen before in the +library. + +You can customize the output format, count the number of duplicate +tracks or albums, and list all tracks that have duplicates or just the +duplicates themselves. These options can either be specified in the +config file:: + + duplicates: + format: $albumartist - $album - $title + count: no + album: no + full: no + +or on the command-line:: + + -f FORMAT, --format=FORMAT + print with custom FORMAT + -c, --count count duplicate tracks or + albums + -a, --album show duplicate albums instead + of tracks + -F, --full show all versions of duplicate + tracks or albums + +format +~~~~~~ + +The ``format`` option (default: :ref:`list_format_item`) lets you +specify a specific format with which to print every track or +album. 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 duplicate +tracks or albums, with ``format`` hard-coded to ``$albumartist - +$album - $title: $count`` or ``$albumartist - $album: $count`` (for +the ``-a`` option). + +album +~~~~~ + +The ``album`` option (default: false) lists duplicate albums instead +of tracks. + +full +~~~~ + +The ``full`` option (default: false) lists every track or album that +has duplicates, not just the duplicates themselves. + +Examples +-------- + +List all duplicate tracks in your collection:: + + beet duplicates + +List all duplicate tracks from 2008:: + + beet duplicates year:2008 + +Print out a unicode histogram of duplicate track years using `spark`_:: + + beet duplicates -f '$year' | spark + ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇ + +Print out a listing of all albums with duplicate tracks, and respective counts:: + + beet duplicates -ac + +The same as the above but include the original album, and show the path:: + + beet duplicates -acf '$path' + + +TODO +---- + +- Allow deleting duplicates. + +.. _spark: https://github.com/holman/spark diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 443784966..db621f2a5 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -62,7 +62,8 @@ disabled by default, but you can turn them on as described above. smartplaylist mbsync missing - + duplicates + Autotagger Extensions '''''''''''''''''''''' @@ -112,7 +113,8 @@ Miscellaneous a different directory. * :doc:`info`: Print music files' tags to the console. * :doc:`missing`: List missing tracks. - +* :doc:`duplicates`: List duplicate tracks or albums. + .. _MPD: http://mpd.wikia.com/ .. _MPD clients: http://mpd.wikia.com/wiki/Clients @@ -147,5 +149,5 @@ plugins `. .. toctree:: :hidden: - + writing From b8f651fa1a44ba442bbe2be615c23bf6961ccae4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 11:44:35 -0700 Subject: [PATCH 06/23] changelog/API docs for #274 --- docs/changelog.rst | 2 ++ docs/plugins/writing.rst | 33 +++++++++++++-------------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 885c2a7c6..c44ecd867 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,8 @@ Changelog Thanks to jayme on GitHub. * :doc:`/plugins/lyrics`: Lyrics searches should now turn up more results due to some fixes in dealing with special characters. +* Plugin-provided template fields now work for both Albums and Items. Thanks + to Pedro Silva. 1.1.0 (April 29, 203) --------------------- diff --git a/docs/plugins/writing.rst b/docs/plugins/writing.rst index 53f0a0656..cff9a166b 100644 --- a/docs/plugins/writing.rst +++ b/docs/plugins/writing.rst @@ -242,35 +242,28 @@ This plugin provides a function ``%initial`` to path templates where ``%initial{$artist}`` expands to the artist's initial (its capitalized first character). -Plugins can also add template *fields*, which are computed values referenced as -``$name`` in templates. To add a new field, decorate a function taking a single -parameter, ``item``, with ``MyPlugin.template_field("name")``. Here's an example -that adds a ``$disc_and_track`` field:: +Plugins can also add template *fields*, which are computed values referenced +as ``$name`` in templates. To add a new field, decorate a function taking a +single parameter, which may be an Item or an Album, with +``MyPlugin.template_field("name")``. Here's an example that adds a +``$disc_and_track`` field:: @MyPlugin.template_field('disc_and_track') - def _tmpl_disc_and_track(item): + def _tmpl_disc_and_track(obj): """Expand to the disc number and track number if this is a multi-disc release. Otherwise, just exapnds to the track number. """ - if item.disctotal > 1: - return u'%02i.%02i' % (item.disc, item.track) + if isinstance(obj, beets.library.Album): + return u'' + if obj.disctotal > 1: + return u'%02i.%02i' % (obj.disc, obj.track) else: - return u'%02i' % (item.track) + return u'%02i' % (obj.track) With this plugin enabled, templates can reference ``$disc_and_track`` as they -can any standard metadata field. - -Note that the above idiom expects the argument ``item`` to be an -actual *track* item. If you'd like to provide a template field for -*albums*, you'll need to check the argument:: - - @MyPlugin.template_field('field') - def _tmpl_field(album): - """Return stuff. - """ - if isinstance(album, beets.library.Album): - return 'stuff' +can any standard metadata field. Since the field is only meaningful for Items, +it expands to the empty string when used in an Album context. Extend MediaFile ^^^^^^^^^^^^^^^^ From 916ceb4b15201675f6dcd16289e1c7faa5415b66 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 11:55:17 -0700 Subject: [PATCH 07/23] changelog and tweaks for #275 I've removed the -p option. The command now always shows plugin-provided template fields if any are available. We also avoid printing out blank lines for plugins that don't provide fields. --- beets/ui/commands.py | 20 ++++++++++---------- docs/changelog.rst | 2 ++ docs/reference/cli.rst | 9 ++++----- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 75fdd5d23..c6096750e 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -91,20 +91,20 @@ def _showdiff(field, oldval, newval): fields_cmd = ui.Subcommand('fields', help='show fields available for queries and format strings') -fields_cmd.parser.add_option('-p', '--plugins', dest='plugins', - action='store_true', - help='show available plugin fields as well') def fields_func(lib, opts, args): - print("Available item fields:") + print("Item fields:") print(" " + "\n ".join([key for key in library.ITEM_KEYS])) - print("\nAvailable album fields:") + + print("\nAlbum fields:") print(" " + "\n ".join([key for key in library.ALBUM_KEYS])) - if opts.plugins: - print("\nAvailable plugin fields:") - for plugin in plugins.find_plugins(): - fields = plugin.template_fields.iteritems() - print(" " + "\n ".join([key for (key, value) in fields])) + + plugin_fields = [] + for plugin in plugins.find_plugins(): + plugin_fields += plugin.template_fields.keys() + if plugin_fields: + print("\nTemplate fields from plugins:") + print(" " + "\n ".join(plugin_fields)) fields_cmd.func = fields_func default_commands.append(fields_cmd) diff --git a/docs/changelog.rst b/docs/changelog.rst index c44ecd867..fb3e97a4d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,8 @@ Changelog to some fixes in dealing with special characters. * Plugin-provided template fields now work for both Albums and Items. Thanks to Pedro Silva. +* The :ref:`fields-cmd` command shows template fields provided by plugins. + Thanks again to Pedro Silva. 1.1.0 (April 29, 203) --------------------- diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index a5e0438ab..cedc4a233 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -246,17 +246,16 @@ Show some statistics on your entire library (if you don't provide a The ``-e`` (``--exact``) option makes the calculation of total file size more accurate but slower. +.. _fields-cmd: + fields `````` :: - beet fields [-p] + beet fields Show the item and album metadata fields available for use in :doc:`query` and -:doc:`pathformat`. - -The ``-p`` (``--plugins``) option shows available plugin fields in -addition to the standard ones. +:doc:`pathformat`. Includes any template fields provided by plugins. Global Flags ------------ From 1ffc56b85aa396c759b2b476c1529a8d84709f46 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 12:07:53 -0700 Subject: [PATCH 08/23] doc correction and refactoring for #275 Plugin-provided template fields are not available for queries. (Perhaps they should be, though!) --- beetsplug/missing.py | 14 ++++++++------ docs/plugins/missing.rst | 7 +++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index ac1561648..1fcb0d68c 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -135,17 +135,19 @@ class MissingPlugin(BeetsPlugin): print(sum([_missing_count(a) for a in albums])) return + # Default format string for count mode. + if count and not fmt: + fmt = '$albumartist - $album: $missing' + for album in albums: if count: missing = _missing_count(album) if missing: - if not fmt: - fmt = '$albumartist - $album: $missing' - print_obj(album, lib, fmt=fmt.format(missing)) - continue + print_obj(album, lib, fmt=fmt) - for item in _missing(album): - print_obj(item, lib, fmt=fmt) + else: + for item in _missing(album): + print_obj(item, lib, fmt=fmt) self._command.func = _miss return [self._command] diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 2418333d5..c128b84b8 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -60,12 +60,11 @@ total The ``total`` option (default: false) prints a single count of missing tracks in all albums -Metadata fields +Template Fields --------------- -With this plugin enabled, templates can reference ``$missing`` as they -can any standard metadata field for use in :doc:`Queries -` and :doc:`Path Formats `. +With this plugin enabled, the ``$missing`` template field expands to the +number of tracks missing from each album. Examples -------- From 1898a79d4f064c5276fa0ea36e6985d6d806d42a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 12:13:01 -0700 Subject: [PATCH 09/23] fix inline for album plugin fields (#274) --- beetsplug/inline.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 81a003cb7..086ed7160 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -18,6 +18,7 @@ import logging import traceback from beets.plugins import BeetsPlugin +from beets.library import Item, Album from beets import config log = logging.getLogger('beets') @@ -46,6 +47,14 @@ def _compile_func(body): eval(code, env) return env[FUNC_NAME] +def _record(obj): + """Get a dictionary of values for an Item or Album object. + """ + if isinstance(obj, Item): + return dict(obj.record) + else: + return dict(obj._record) + def compile_inline(python_code): """Given a Python expression or function body, compile it as a path field function. The returned function takes a single argument, an @@ -70,8 +79,8 @@ def compile_inline(python_code): if is_expr: # For expressions, just evaluate and return the result. - def _expr_func(item): - values = dict(item.record) + def _expr_func(obj): + values = _record(obj) try: return eval(code, values) except Exception as exc: @@ -80,8 +89,8 @@ def compile_inline(python_code): else: # For function bodies, invoke the function with values as global # variables. - def _func_func(item): - func.__globals__.update(item.record) + def _func_func(obj): + func.__globals__.update(_record(obj)) try: return func() except Exception as exc: From 0bb51e9f1bcb5577ddd28c889dc59a61fda71986 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 12:20:27 -0700 Subject: [PATCH 10/23] thanks and doc tweaks for duplicates (#278) --- docs/changelog.rst | 4 ++-- docs/plugins/duplicates.rst | 8 +++----- docs/reference/pathformat.rst | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index db2f2f6bb..2f659f4b7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,9 +5,9 @@ Changelog ---------------------- * New :doc:`/plugins/duplicates`: Find tracks or albums in your - library that are **duplicated**. + library that are **duplicated**. Thanks to Pedro Silva. * New :doc:`/plugins/missing`: Find albums in your library that are **missing - tracks**. Thanks to Pedro Silva. + tracks**. Thanks once more to Pedro Silva. * Your library now keeps track of **when music was added** to it. The new ``added`` field is a timestamp reflecting when each item and album was imported and the new ``%time{}`` template function lets you format this diff --git a/docs/plugins/duplicates.rst b/docs/plugins/duplicates.rst index f067896ec..68edbf325 100644 --- a/docs/plugins/duplicates.rst +++ b/docs/plugins/duplicates.rst @@ -1,5 +1,5 @@ Duplicates Plugin -============== +================= This plugin adds a new command, ``duplicates`` or ``dup``, which finds and lists duplicate tracks or albums in your collection. @@ -8,11 +8,9 @@ Installation ------------ Enable the plugin by putting ``duplicates`` on your ``plugins`` line in -:doc:`config file `:: +your :doc:`config file `:: - plugins: - duplicates - ... + plugins: duplicates Configuration ------------- diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 723819182..53a8e1720 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -71,7 +71,7 @@ These functions are built in to beets: * ``%aunique{identifiers,disambiguators}``: Provides a unique string to disambiguate similar albums in the database. See :ref:`aunique`, below. * ``%time{date_time,format}``: Return the date and time in any format accepted - by `strfime`_. For example, to get the year some music was added to your + by `strftime`_. For example, to get the year some music was added to your library, use ``%time{$added,%Y}``. .. _unidecode module: http://pypi.python.org/pypi/Unidecode From cee3b2063027917f5b0dc1716469a97e9fe29a18 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 12:35:35 -0700 Subject: [PATCH 11/23] duplicates: refactoring and empty-ID fix (#278) The main change here, aside from documentation/naming updates, is that we skip "duplicates" that arise from albums/tracks that are missing their MBIDs. --- beetsplug/duplicates.py | 45 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 9b037b3ed..34f1ec846 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -23,25 +23,26 @@ PLUGIN = 'duplicates' log = logging.getLogger('beets') -def _counts(items): - """Return count of ITEMS indexed by item_id. +def _group_by_id(objs): + """Return a dictionary whose keys are MBIDs and whose values are + lists of objects (Albums or Items) with that ID. """ import collections counts = collections.defaultdict(list) - for item in items: - item_id = getattr(item, 'mb_trackid', item.mb_albumid) - counts[item_id].append(item) + for obj in objs: + mbid = getattr(obj, 'mb_trackid', obj.mb_albumid) + counts[mbid].append(obj) return counts -def _duplicates(items, full): - """Return duplicate ITEMS. +def _duplicates(objs, full): + """Generate triples of MBIDs, duplicate counts, and constituent + objects. """ - counts = _counts(items) offset = 0 if full else 1 - for item_id, items in counts.iteritems(): - if len(items) > 1: - yield (item_id, len(items)-offset, items[offset:]) + for mbid, objs in _group_by_id(objs).iteritems(): + if len(objs) > 1: + yield (mbid, len(objs) - offset, objs[offset:]) class DuplicatesPlugin(BeetsPlugin): @@ -92,18 +93,18 @@ class DuplicatesPlugin(BeetsPlugin): else: items = lib.items(decargs(args)) - orig_fmt = fmt + # Default format string for count mode. + if count and not fmt: + if album: + fmt = '$albumartist - $album' + else: + fmt = '$albumartist - $album - $title' + fmt += ': {}' + for obj_id, obj_count, objs in _duplicates(items, full): - if count: - if not fmt: - if album: - fmt = '$albumartist - $album' - else: - fmt = '$albumartist - $album - $title' - fmt += ': {}' - for o in objs: - print_obj(o, lib, fmt=fmt.format(obj_count)) - fmt = orig_fmt + if obj_id: # Skip empty IDs. + for o in objs: + print_obj(o, lib, fmt=fmt.format(obj_count)) self._command.func = _dup return [self._command] From 42624eb74546893ddb1459c8d1f67e12ff1f4eb4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 19:05:16 -0700 Subject: [PATCH 12/23] use lazy config loader in confit This lets you, for example, import beets in a library or the REPL and use it immediately without calling "beets.config.read()" first. --- beets/__init__.py | 2 +- beets/ui/__init__.py | 9 ++------- beets/util/confit.py | 33 +++++++++++++++++++++++++++++++++ test/_common.py | 1 + test/test_ui.py | 2 +- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index ebc3eedcd..151b46994 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -20,4 +20,4 @@ from beets.util import confit Library = beets.library.Library -config = confit.Configuration('beets', __name__, False) +config = confit.LazyConfig('beets', __name__) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 9e92ce4a4..099ea4cc3 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -689,18 +689,13 @@ class SubcommandsOptionParser(optparse.OptionParser): # The root parser and its main function. -def _raw_main(args, load_config=True): +def _raw_main(args): """A helper function for `main` without top-level exception handling. """ - # Load global configuration files. - if load_config: - config.read() - # Temporary: Migrate from 1.0-style configuration. from beets.ui import migrate - if load_config: - migrate.automigrate() + migrate.automigrate() # Get the default subcommands. from beets.ui.commands import default_commands diff --git a/beets/util/confit.py b/beets/util/confit.py index b7af4ea40..6a787ce3c 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -677,3 +677,36 @@ class Configuration(RootView): if not os.path.isdir(appdir): os.makedirs(appdir) return appdir + +class LazyConfig(Configuration): + """A Configuration at reads files on demand when it is first + accessed. This is appropriate for using as a global config object at + the module level. + """ + def __init__(self, appname, modname=None): + super(LazyConfig, self).__init__(appname, modname, False) + self._materialized = False # Have we read the files yet? + self._lazy_prefix = [] # Pre-materialization calls to set(). + self._lazy_suffix = [] # Calls to add(). + + def resolve(self): + if not self._materialized: + self._materialized = True + self.read() + self.sources += self._lazy_suffix + self.sources[:0] = self._lazy_prefix + return super(LazyConfig, self).resolve() + + def add(self, value): + super(LazyConfig, self).add(value) + if not self._materialized: + # Buffer additions to end. + self._lazy_suffix += self.sources + del self.sources[:] + + def set(self, value): + super(LazyConfig, self).set(value) + if not self._materialized: + # Buffer additions to beginning. + self._lazy_prefix[:0] = self.sources + del self.sources[:] diff --git a/test/_common.py b/test/_common.py index 864813766..0a2f8595b 100644 --- a/test/_common.py +++ b/test/_common.py @@ -92,6 +92,7 @@ class TestCase(unittest.TestCase): def setUp(self): # A "clean" source list including only the defaults. beets.config.sources = [] + beets.config._materialized = True beets.config.read(user=False, defaults=True) # Direct paths to a temporary directory. Tests can also use this diff --git a/test/test_ui.py b/test/test_ui.py index 0a286d125..c9d57a466 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -478,7 +478,7 @@ class ConfigTest(_common.TestCase): if config_yaml: config_data = yaml.load(config_yaml, Loader=confit.Loader) config.set(config_data) - ui._raw_main(args + ['test'], False) + ui._raw_main(args + ['test']) def test_paths_section_respected(self): def func(lib, opts, args): From 4d62a832104f554d9106f0f555c95d4735e13228 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 17 May 2013 19:10:44 -0700 Subject: [PATCH 13/23] simplify materialization in LazyConfig --- beets/util/confit.py | 6 +++++- test/_common.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/beets/util/confit.py b/beets/util/confit.py index 6a787ce3c..6499f146d 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -689,9 +689,13 @@ class LazyConfig(Configuration): self._lazy_prefix = [] # Pre-materialization calls to set(). self._lazy_suffix = [] # Calls to add(). + def read(self, user=True, defaults=True): + self._materialized = True + super(LazyConfig, self).read(user, defaults) + def resolve(self): if not self._materialized: - self._materialized = True + # Read files and unspool buffers. self.read() self.sources += self._lazy_suffix self.sources[:0] = self._lazy_prefix diff --git a/test/_common.py b/test/_common.py index 0a2f8595b..864813766 100644 --- a/test/_common.py +++ b/test/_common.py @@ -92,7 +92,6 @@ class TestCase(unittest.TestCase): def setUp(self): # A "clean" source list including only the defaults. beets.config.sources = [] - beets.config._materialized = True beets.config.read(user=False, defaults=True) # Direct paths to a temporary directory. Tests can also use this From 5dbff4a8eb204ec6addaa5b30e401ee5fe3f0cf3 Mon Sep 17 00:00:00 2001 From: Georges Dubus Date: Sun, 19 May 2013 17:22:19 +0200 Subject: [PATCH 14/23] Fixed the randomness of the equal change to have more than one song by artist --- beetsplug/random.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/beetsplug/random.py b/beetsplug/random.py index d543cfd1b..004b0f6d6 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -21,6 +21,7 @@ from beets.util.functemplate import Template import random from operator import attrgetter from itertools import groupby +import collections def random_item(lib, opts, args): query = decargs(args) @@ -34,18 +35,29 @@ def random_item(lib, opts, args): objs = list(lib.albums(query=query)) else: objs = list(lib.items(query=query)) + if opts.equal_chance: key = attrgetter('albumartist') objs.sort(key=key) - # Now the objs are list of albums or items - objs = [list(v) for k, v in groupby(objs, key)] - number = min(len(objs), opts.number) - objs = random.sample(objs, number) + # {artists: objects} + objs_by_artists = {artist: list(v) for artist, v in groupby(objs, key)} + artists = objs_by_artists.keys() - if opts.equal_chance: - # Select one random item from that artist - objs = map(random.choice, objs) + # {artist: count} + selected_artists = collections.defaultdict(int) + for _ in range(opts.number): + selected_artists[random.choice(artists)] += 1 + + objs = [] + for artist, count in selected_artists.items(): + objs_from_artist = objs_by_artists[artist] + number = min(count, len(objs_from_artist)) + objs.extend(random.sample(objs_from_artist, number)) + + else: + number = min(len(objs), opts.number) + objs = random.sample(objs, number) for item in objs: print_obj(item, lib, template) From 71666c7ac2f54ad9c2ef1bd64159668d1e617598 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 10:05:23 -0700 Subject: [PATCH 15/23] fix #280: unicode error when formatting artpath The `artpath` field for Albums is a bytestring, not a Unicode string, and this was causing a UnicodeDecodeError in `format_for_path`. --- beets/library.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beets/library.py b/beets/library.py index 8d9060ad2..a8fe0be73 100644 --- a/beets/library.py +++ b/beets/library.py @@ -196,12 +196,12 @@ def format_for_path(value, key=None, pathmod=None): pathmod = pathmod or os.path if isinstance(value, basestring): + if isinstance(value, str): + value = value.decode('utf8', 'ignore') + sep_repl = beets.config['path_sep_replace'].get(unicode) for sep in (pathmod.sep, pathmod.altsep): if sep: - value = value.replace( - sep, - beets.config['path_sep_replace'].get(unicode), - ) + value = value.replace(sep, sep_repl) elif key in ('track', 'tracktotal', 'disc', 'disctotal'): # Pad indices with zeros. value = u'%02i' % (value or 0) From c56ef31da04ff9a847a1eb07a2583769b7e89f88 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 10:18:41 -0700 Subject: [PATCH 16/23] remove art_filename Library field This is the first of a handful of refactorings that take advantage of the new confit system to simplify parameter passing boilerplate. --- beets/library.py | 8 ++------ beets/ui/__init__.py | 1 - test/test_db.py | 3 ++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/beets/library.py b/beets/library.py index a8fe0be73..18c0afb06 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1113,7 +1113,6 @@ class Library(BaseLibrary): directory='~/Music', path_formats=((PF_KEY_DEFAULT, '$artist/$album/$track $title'),), - art_filename='cover', timeout=5.0, replacements=None, item_fields=ITEM_FIELDS, @@ -1124,7 +1123,6 @@ class Library(BaseLibrary): self.path = bytestring_path(normpath(path)) self.directory = bytestring_path(normpath(directory)) self.path_formats = path_formats - self.art_filename = art_filename self.replacements = replacements self._memotable = {} # Used for template substitution performance. @@ -1703,11 +1701,9 @@ class Album(BaseAlbum): image = bytestring_path(image) item_dir = item_dir or self.item_dir() - if not isinstance(self._library.art_filename,Template): - self._library.art_filename = Template(self._library.art_filename) - + filename_tmpl = Template(beets.config['art_filename'].get(unicode)) subpath = util.sanitize_path(format_for_path( - self.evaluate_template(self._library.art_filename) + self.evaluate_template(filename_tmpl) )) subpath = bytestring_path(subpath) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 099ea4cc3..e5b78e521 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -729,7 +729,6 @@ def _raw_main(args): dbpath, config['directory'].as_filename(), get_path_formats(), - Template(config['art_filename'].get(unicode)), config['timeout'].as_number(), get_replacements(), ) diff --git a/test/test_db.py b/test/test_db.py index 6dc822e63..f3c3004ed 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -29,6 +29,7 @@ from _common import item import beets.library from beets import util from beets import plugins +from beets import config TEMP_LIB = os.path.join(_common.RSRC, 'test_copy.blb') @@ -835,10 +836,10 @@ class BaseAlbumTest(_common.TestCase): class ArtDestinationTest(_common.TestCase): def setUp(self): super(ArtDestinationTest, self).setUp() + config['art_filename'] = u'artimage' self.lib = beets.library.Library(':memory:') self.i = item() self.i.path = self.lib.destination(self.i) - self.lib.art_filename = 'artimage' self.ai = self.lib.add_album((self.i,)) def test_art_filename_respects_setting(self): From 3e740b7d24045d83f0a37d267f15f1dbedb740ed Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 10:19:52 -0700 Subject: [PATCH 17/23] remove timeout Library field --- beets/library.py | 7 ++++--- beets/ui/__init__.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/beets/library.py b/beets/library.py index 18c0afb06..df5d7cbf2 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1113,7 +1113,6 @@ class Library(BaseLibrary): directory='~/Music', path_formats=((PF_KEY_DEFAULT, '$artist/$album/$track $title'),), - timeout=5.0, replacements=None, item_fields=ITEM_FIELDS, album_fields=ALBUM_FIELDS): @@ -1127,7 +1126,6 @@ class Library(BaseLibrary): self._memotable = {} # Used for template substitution performance. - self.timeout = timeout self._connections = {} self._tx_stacks = defaultdict(list) # A lock to protect the _connections and _tx_stacks maps, which @@ -1208,7 +1206,10 @@ class Library(BaseLibrary): return self._connections[thread_id] else: # Make a new connection. - conn = sqlite3.connect(self.path, timeout=self.timeout) + conn = sqlite3.connect( + self.path, + timeout=beets.config['timeout'].as_number(), + ) # Access SELECT results like dictionaries. conn.row_factory = sqlite3.Row diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index e5b78e521..9dd568689 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -729,7 +729,6 @@ def _raw_main(args): dbpath, config['directory'].as_filename(), get_path_formats(), - config['timeout'].as_number(), get_replacements(), ) except sqlite3.OperationalError: From 475228a5e83560c4a816feeead2251f6d3a1e3f6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 10:34:46 -0700 Subject: [PATCH 18/23] fix #281: album art filename respects replace --- beets/library.py | 6 +++--- beets/util/__init__.py | 9 +++------ docs/changelog.rst | 1 + test/test_db.py | 10 +++++++++- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/beets/library.py b/beets/library.py index df5d7cbf2..8a1687914 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1703,9 +1703,9 @@ class Album(BaseAlbum): item_dir = item_dir or self.item_dir() filename_tmpl = Template(beets.config['art_filename'].get(unicode)) - subpath = util.sanitize_path(format_for_path( - self.evaluate_template(filename_tmpl) - )) + subpath = format_for_path(self.evaluate_template(filename_tmpl)) + subpath = util.sanitize_path(subpath, + replacements=self._library.replacements) subpath = bytestring_path(subpath) _, ext = os.path.splitext(image) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index cb1c12eaf..d94b14e22 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -468,14 +468,11 @@ def sanitize_path(path, pathmod=None, replacements=None): reliably on Windows when a path begins with a drive letter. Path separators (including altsep!) should already be cleaned from the path components. If replacements is specified, it is used *instead* - of the default set of replacements for the platform; it must be a - list of (compiled regex, replacement string) pairs. + of the default set of replacements; it must be a list of (compiled + regex, replacement string) pairs. """ pathmod = pathmod or os.path - - # Choose the appropriate replacements. - if not replacements: - replacements = list(CHAR_REPLACE) + replacements = replacements or CHAR_REPLACE comps = components(path, pathmod) if not comps: diff --git a/docs/changelog.rst b/docs/changelog.rst index 2f659f4b7..169dd5a8c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,7 @@ Changelog to Pedro Silva. * The :ref:`fields-cmd` command shows template fields provided by plugins. Thanks again to Pedro Silva. +* Album art filenames now respect the :ref:`replace` configuration. 1.1.0 (April 29, 203) --------------------- diff --git a/test/test_db.py b/test/test_db.py index f3c3004ed..285a136be 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -837,7 +837,10 @@ class ArtDestinationTest(_common.TestCase): def setUp(self): super(ArtDestinationTest, self).setUp() config['art_filename'] = u'artimage' - self.lib = beets.library.Library(':memory:') + config['replace'] = {u'X': u'Y'} + self.lib = beets.library.Library( + ':memory:', replacements=[(re.compile(u'X'), u'Y')] + ) self.i = item() self.i.path = self.lib.destination(self.i) self.ai = self.lib.add_album((self.i,)) @@ -851,6 +854,11 @@ class ArtDestinationTest(_common.TestCase): track = self.lib.destination(self.i) self.assertEqual(os.path.dirname(art), os.path.dirname(track)) + def test_art_path_sanitized(self): + config['art_filename'] = u'artXimage' + art = self.ai.art_destination('something.jpg') + self.assert_('artYimage' in art) + class PathStringTest(_common.TestCase): def setUp(self): super(PathStringTest, self).setUp() From 90b4092e4f4a40d1434d03fb74cbc0b32c137912 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 10:44:35 -0700 Subject: [PATCH 19/23] changelog & docs typo fixes for #263 --- docs/changelog.rst | 3 +++ docs/plugins/random.rst | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 169dd5a8c..7a56160fa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,9 @@ Changelog **numeric ranges**. For example, you can get a list of albums from the '90s by typing ``beet ls year:1990..1999`` or find high-bitrate music with ``bitrate:128000..``. See :ref:`numericquery`. Thanks to Michael Schuerig. +* :doc:`/plugins/random`: A new ``-e`` option gives an equal chance to each + artist in your collection to avoid biasing random samples to prolific + artists. Thanks to Georges Dubus. * The :ref:`modify-cmd` now correctly converts types when modifying non-string fields. You can now safely modify the "comp" flag and the "year" field, for example. Thanks to Lucas Duailibe. diff --git a/docs/plugins/random.rst b/docs/plugins/random.rst index ef8737b52..59e4b9483 100644 --- a/docs/plugins/random.rst +++ b/docs/plugins/random.rst @@ -17,7 +17,7 @@ track, use ``-a``; to print paths to items instead of metadata, use ``-p``; and to use a custom format for printing, use ``-f FORMAT``. If the ``-e`` option is passed, the random choice will be even among -artists (the albumartist field) This makes sure that your antholofgy +artists (the albumartist field). This makes sure that your anthology of Bob Dylan won't make you listen to Bob Dylan 50% of the time. The ``-n NUMBER`` option controls the number of objects that are selected and From 6f2c1d81bd140afd683c3ec3c71c8c4fb73b8ad9 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 11:00:52 -0700 Subject: [PATCH 20/23] random -e (#263): fix some short responses This slight modification to the selection algorithm avoids the situation in which too many objects are chosen for a given artist and fewer than N objects are eventually returned. We do this by implementing "selection without replacement" literally: we choose objects one at a time and pop them from the population when they are selected. --- beetsplug/random.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/beetsplug/random.py b/beetsplug/random.py index 004b0f6d6..f50061bfb 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -37,23 +37,27 @@ def random_item(lib, opts, args): objs = list(lib.items(query=query)) if opts.equal_chance: + # Group the objects by artist so we can sample from them. key = attrgetter('albumartist') objs.sort(key=key) - - # {artists: objects} objs_by_artists = {artist: list(v) for artist, v in groupby(objs, key)} - artists = objs_by_artists.keys() - - # {artist: count} - selected_artists = collections.defaultdict(int) - for _ in range(opts.number): - selected_artists[random.choice(artists)] += 1 objs = [] - for artist, count in selected_artists.items(): + for _ in range(opts.number): + # Terminate early if we're out of objects to select. + if not objs_by_artists: + break + + # Choose an artist and an object for that artist, removing + # this choice from the pool. + artist = random.choice(objs_by_artists.keys()) objs_from_artist = objs_by_artists[artist] - number = min(count, len(objs_from_artist)) - objs.extend(random.sample(objs_from_artist, number)) + i = random.randint(0, len(objs_from_artist) - 1) + objs.append(objs_from_artist.pop(i)) + + # Remove the artist if we've used up all of its objects. + if not objs_from_artist: + del objs_by_artists[artist] else: number = min(len(objs), opts.number) From 45baa6efe13d41035da106b9175234d848ea75dd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 17:35:23 -0700 Subject: [PATCH 21/23] first stab at "advanced guide" --- docs/guides/advanced.rst | 106 ++++++++++++++++++++++++++++++++++ docs/guides/index.rst | 1 + docs/plugins/index.rst | 2 + docs/reference/cli.rst | 2 + docs/reference/pathformat.rst | 2 + 5 files changed, 113 insertions(+) create mode 100644 docs/guides/advanced.rst diff --git a/docs/guides/advanced.rst b/docs/guides/advanced.rst new file mode 100644 index 000000000..876e3cad3 --- /dev/null +++ b/docs/guides/advanced.rst @@ -0,0 +1,106 @@ +Advanced Awesomeness +==================== + +So you have beets up and running and you've started :doc:`importing your +music `. There's a lot more that beets can do now that it has +cataloged your collection. Here's a few features to get you started. + +Most of these tips involve :doc:`plugins ` and fiddling with beets' +:doc:`configuration `. So use your favorite text editor +create a config file before you continue. + + +Fetch album art, genres, and lyrics +----------------------------------- + +Beets can help you fill in more than just the basic taxonomy metadata that +comes from MusicBrainz. Plugins can provide :doc:`album art +`, :doc:`lyrics `, and +:doc:`genres ` from databases around the Web. + +If you want beets to get any of this data automatically during the import +process, just enable any of the three relevant plugins (see +:ref:`using-plugins`). For example, put this line in your :doc:`config file +` to enable all three:: + + plugins: fetchart lyrics lastgenre + +Each plugin also has a command you can run to fetch data manually. For +example, if you want to get lyrics for all the Beatles tracks in your +collection, just type ``beet lyrics beatles`` after enabling the plugin. + +Read more about using each of these plugins: + +* :doc:`/plugins/fetchart` (and its accompanying :doc:`/plugins/embedart`) +* :doc:`/plugins/lyrics` +* :doc:`/plugins/lastgenre` + + +Customize your file and folder names +------------------------------------ + +Beets uses an extremely flexible template system to name the folders and files +that organize your music in your filesystem. Take a look at +:ref:`path-format-config` for the basics: use fields like ``$year`` and +``$title`` to build up a naming scheme. But if you need more flexibility, +there are two features you need to know about: + +* :ref:`Template functions ` are simple expressions you + can use in your path formats to add logic to your names. For example, you + can get an artist's first initial using ``%upper{%left{$albumartist,1}}``. +* If you need more flexibility, the :doc:`/plugins/inline` lets you write + snippets of Python code that generate parts of your filenames. The + equivalent code for getting an artist initial with the *inline* plugin looks + like ``initial: albumartist[0].upper()``. + +If you already have music in your library and want to update their names +according to a new scheme, just run the :ref:`move-cmd` command to rename +everything. + + +Stream your music to another computer +------------------------------------- + +Sometimes it can be really convenient to store your music on one machine and +play it on another. For example, I like to keep my music on a server at home +but play it at work (without copying my whole library locally). The +:doc:`/plugins/web` makes streaming your music easy---it's sort of like having +your own personal Spotify. + +First, enable the ``web`` plugin (see :ref:`using-plugins`). Run the server by +typing ``beet web`` and head to http://localhost:8337 in a browser. You can +browse your collection with queries and, if your browser supports it, play +music using HTML5 audio. + +But for a great listening experience, pair beets with the `Tomahawk`_ music +player. Tomahawk lets you listen to music from many different sources, +including a beets server. Just download Tomahawk and open its settings to +connect it to beets. `A post on the beets blog`_ has a more detailed guide. + +.. _A post on the beets blog: + http://beets.radbox.org/blog/tomahawk-resolver.html +.. _Tomahawk: http://www.tomahawk-player.org + + +Transcode music files for media players +--------------------------------------- + +Do you ever find yourself transcoding high-quality rips to a lower-bitrate, +lossy format for your phone or music player? Beets can help with that. + +You'll first need to install `ffmpeg`_. Then, enable beets' +:doc:`/plugins/convert`. Set a destination directory in your +:doc:`config file ` like so:: + + convert: + dest: ~/converted_music + +Then, use the command ``beet convert QUERY`` to transcode everything matching +the query and drop the resulting files in that directory, named according to +your path formats. For example, ``beet convert long winters`` will move over +everything by the Long Winters for listening on the go. + +The plugin has many more dials you can fiddle with to get your conversions how +you like them. Check out :doc:`its documentation `. + +.. _ffmpeg: http://www.ffmpeg.org diff --git a/docs/guides/index.rst b/docs/guides/index.rst index d9ec06d9b..9269b249e 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -10,4 +10,5 @@ guide. main tagger + advanced migration diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index db621f2a5..2ad472066 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -5,6 +5,8 @@ Plugins can extend beets' core functionality. Plugins can add new commands to the command-line interface, respond to events in beets, augment the autotagger, or provide new path template functions. +.. _using-plugins: + Using Plugins ------------- diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index cedc4a233..ff8d0f8a8 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -199,6 +199,8 @@ overridden with ``-w`` (write tags, the default) and ``-W`` (don't write tags). Finally, this command politely asks for your permission before making any changes, but you can skip that prompt with the ``-y`` switch. +.. _move-cmd: + move ```` :: diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 53a8e1720..f037b4395 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -43,6 +43,8 @@ probably don't want that! So use ``$albumartist``. As a convenience, however, beets allows ``$albumartist`` to fall back to the value for ``$artist`` and vice-versa if one tag is present but the other is not. +.. _template-functions: + Functions --------- From f3962e62754f4acbdf71e633ce851c99f38dbe0c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 19 May 2013 17:43:10 -0700 Subject: [PATCH 22/23] link to the advanced guide from "getting started" --- docs/guides/advanced.rst | 6 +++--- docs/guides/main.rst | 36 +++++++----------------------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/docs/guides/advanced.rst b/docs/guides/advanced.rst index 876e3cad3..fec105e4d 100644 --- a/docs/guides/advanced.rst +++ b/docs/guides/advanced.rst @@ -5,9 +5,9 @@ So you have beets up and running and you've started :doc:`importing your music `. There's a lot more that beets can do now that it has cataloged your collection. Here's a few features to get you started. -Most of these tips involve :doc:`plugins ` and fiddling with beets' -:doc:`configuration `. So use your favorite text editor -create a config file before you continue. +Most of these tips involve :doc:`plugins ` and fiddling with +beets' :doc:`configuration `. So use your favorite text +editor create a config file before you continue. Fetch album art, genres, and lyrics diff --git a/docs/guides/main.rst b/docs/guides/main.rst index 84124add1..4a1b55191 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -152,9 +152,8 @@ metadata for every album you import. Option (a) is really fast, but option (b) makes sure all your songs' tags are exactly right from the get-go. The point about speed bears repeating: using the autotagger on a large library can take a very long time, and it's an interactive process. So set aside a good chunk of -time if you're going to go that route. (I'm working on improving the -autotagger's performance and automation.) For more information on the -interactive tagging process, see :doc:`tagger`. +time if you're going to go that route. For more on the interactive +tagging process, see :doc:`tagger`. If you've got time and want to tag all your music right once and for all, do this:: @@ -228,35 +227,14 @@ you have:: Artists: 548 Albums: 1094 -Playing Music -------------- - -Beets is primarily intended as a music organizer, not a player. It's designed to -be used in conjunction with other players (consider `Decibel`_ or `cmus`_; -there's even :ref:`a cmus plugin for beets `). However, it does -include a simple music player---it doesn't have a ton of features, but it gets -the job done. - -.. _Decibel: http://decibel.silent-blade.org/ -.. _cmus: http://cmus.sourceforge.net/ - -The player, called BPD, is a clone of an excellent music player called `MPD`_. -Like MPD, it runs as a daemon (i.e., without a user interface). Another program, -called an MPD client, controls the player and provides the user with an -interface. You'll need to enable the BPD plugin before you can use it. Check out -:doc:`/plugins/bpd`. - -.. _MPD: http://mpd.wikia.com/ - -You can, of course, use the bona fide MPD server with your beets library. MPD is -a great player and has more features than BPD. BPD just provides a convenient, -built-in player that integrates tightly with your beets database. - Keep Playing ------------ -The :doc:`/reference/cli` page has more detailed description of all of beets' -functionality. (Like deleting music! That's important.) Start exploring! +This is only the beginning of your long and prosperous journey with beets. To +keep learning, take a look at :doc:`advanced` for a sampling of what else +is possible. You'll also want to glance over the :doc:`/reference/cli` page +for a more detailed description of all of beets' functionality. (Like +deleting music! That's important.) Also, check out :ref:`included-plugins` as well as :ref:`other-plugins`. The real power of beets is in its extensibility---with plugins, beets can do almost From e1f75e8e913752d97b717abd9d9839bb543ef671 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 20 May 2013 09:40:05 -0700 Subject: [PATCH 23/23] travis: retry package installation http://about.travis-ci.org/blog/2013-05-20-network-timeouts-build-retries/ --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ebb7162fb..746f430ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ python: - "2.7" - "2.6" install: - - pip install . --use-mirrors - - pip install pylast flask --use-mirrors + - travis_retry pip install . --use-mirrors + - travis_retry pip install pylast flask --use-mirrors - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2 --use-mirrors; fi" script: nosetests branches: