diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index adc47d095..eaf79a0ce 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1172,7 +1172,7 @@ def _raw_main(args, lib=None): parser.add_option('-d', '--directory', dest='directory', help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='count', - help='print debugging information') + help='log more details (use twice for even more)') parser.add_option('-c', '--config', dest='config', help='path to configuration file') parser.add_option('-h', '--help', dest='help', action='store_true', diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 39501f361..25ed0425d 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1428,10 +1428,18 @@ def move_items(lib, dest, query, copy, album, pretend): items, albums = _do_query(lib, query, album, False) objs = albums if album else items + # Filter out files that don't need to be moved. + isitemmoved = lambda item: item.path != item.destination(basedir=dest) + isalbummoved = lambda album: any(isitemmoved(i) for i in album.items()) + objs = [o for o in objs if (isalbummoved if album else isitemmoved)(o)] + action = 'Copying' if copy else 'Moving' entity = 'album' if album else 'item' log.info(u'{0} {1} {2}{3}.', action, len(objs), entity, - 's' if len(objs) > 1 else '') + 's' if len(objs) != 1 else '') + if not objs: + return + if pretend: if album: show_path_changes([(item.path, item.destination(basedir=dest)) diff --git a/beetsplug/acousticbrainz.py b/beetsplug/acousticbrainz.py new file mode 100644 index 000000000..934531a3c --- /dev/null +++ b/beetsplug/acousticbrainz.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2015-2016, Ohm Patel. +# +# 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. + +""" Fetch various AcousticBrainz metadata using MBID +""" +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +import requests + +from beets import plugins, ui + +ACOUSTIC_URL = "http://acousticbrainz.org/" +LEVEL = "/high-level" + + +class AcousticPlugin(plugins.BeetsPlugin): + def __init__(self): + super(AcousticPlugin, self).__init__() + + def commands(self): + cmd = ui.Subcommand('acousticbrainz', + help="fetch metadata from AcousticBrainz") + + def func(lib, opts, args): + items = lib.items(ui.decargs(args)) + fetch_info(self._log, items) + + cmd.func = func + return [cmd] + + +def fetch_info(log, items): + """Currently outputs MBID and corresponding request status code + """ + for item in items: + if item.mb_trackid: + log.info('getting data for: {}', item) + + # Fetch the data from the AB API. + url = generate_url(item.mb_trackid) + log.debug('fetching URL: {}', url) + try: + rs = requests.get(url) + except requests.RequestException as exc: + log.info('request error: {}', exc) + continue + + # Check for missing tracks. + if rs.status_code == 404: + log.info('recording ID {} not found', item.mb_trackid) + continue + + # Parse the JSON response. + try: + data = rs.json() + except ValueError: + log.debug('Invalid Response: {}', rs.text) + + # Get each field and assign it on the item. + item.danceable = get_value( + log, + data, + ["highlevel", "danceability", "all", "danceable"], + ) + item.mood_happy = get_value( + log, + data, + ["highlevel", "mood_happy", "all", "happy"], + ) + item.mood_party = get_value( + log, + data, + ["highlevel", "mood_party", "all", "party"], + ) + + # Store the data. We only update flexible attributes, so we + # don't call `item.try_write()` here. + item.store() + + +def generate_url(mbid): + """Generates url of AcousticBrainz end point for given MBID + """ + return ACOUSTIC_URL + mbid + LEVEL + + +def get_value(log, data, map_path): + """Allows traversal of dictionary with cleaner formatting + """ + try: + return reduce(lambda d, k: d[k], map_path, data) + except KeyError: + log.debug('Invalid Path: {}', map_path) diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 5e4522ff9..182c7f9a2 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -465,7 +465,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): def commands(self): fetch_cmd = ui.Subcommand('echonest', - help='Fetch metadata from the EchoNest') + help='fetch metadata from The Echo Nest') fetch_cmd.parser.add_option( '-f', '--force', dest='force', action='store_true', default=False, help='(re-)download information from the EchoNest' diff --git a/beetsplug/play.py b/beetsplug/play.py index 1d425e1c8..c943a5155 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -129,13 +129,8 @@ class PlayPlugin(BeetsPlugin): try: util.interactive_open(open_args, command_str) except OSError as exc: - raise ui.UserError("Could not play the music playlist: " + raise ui.UserError("Could not play the query: " "{0}".format(exc)) - finally: - if not raw: - self._log.debug('Removing temporary playlist: {}', - open_args[0]) - util.remove(open_args[0]) def _create_tmp_playlist(self, paths_list): """Create a temporary .m3u file. Return the filename. diff --git a/docs/changelog.rst b/docs/changelog.rst index b69288170..a6954ef2b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,10 +9,26 @@ New: * :doc:`/plugins/fetchart`: The Google Images backend has been restored. It now requires an API key from Google. Thanks to :user:`lcharlick`. :bug:`1778` +* A new :doc:`/plugins/acousticbrainz` fetches acoustic-analysis information + from the `AcousticBrainz`_ project. Thanks to :user:`opatel99`. :bug:`1784` * A new :doc:`/plugins/mbsubmit` lets you print the tracks of an album in a format parseable by MusicBrainz track parser during an interactive import session. :bug:`1779` +.. _AcousticBrainz: http://acousticbrainz.org/ + +Fixes: + +* :doc:`/plugins/play`: Remove dead code. From this point on, beets isn't + supposed and won't try to delete the playlists generated by ``beet play`` + (Note that although it was supposed to, beet didn't actually remove the + generated ``.m3u`` files beforehand either.). If this is an issue for you, you + might want to take a look at the ``raw`` config option of the + :doc:`/plugins/play`. :bug:`1785`, :bug:`1600` +* The :ref:`move-cmd` command does not display files whose path does not change + anymore. :bug:`1583` + + 1.3.16 (December 28, 2015) -------------------------- diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 3b3ad80a2..b7c5d82f9 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -517,16 +517,18 @@ str.format-style string formatting. So you can write logging calls like this:: When beets is in verbose mode, plugin messages are prefixed with the plugin name to make them easier to see. -What messages will be logged depends on the logging level and the action +Which messages will be logged depends on the logging level and the action performed: -* On import stages and event handlers, the default is ``WARNING`` messages and - above. -* On direct actions, the default is ``INFO`` or above, as with the rest of - beets. +* Inside import stages and event handlers, the default is ``WARNING`` messages + and above. +* Everywhere else, the default is ``INFO`` or above. -The verbosity can be increased with ``--verbose`` flags: each flags lowers the -level by a notch. +The verbosity can be increased with ``--verbose`` (``-v``) flags: each flags +lowers the level by a notch. That means that, with a single ``-v`` flag, event +handlers won't have their ``DEBUG`` messages displayed, but command functions +(for example) will. With ``-vv`` on the command line, ``DEBUG`` messages will +be displayed everywhere. This addresses a common pattern where plugins need to use the same code for a command and an import stage, but the command needs to print more messages than diff --git a/docs/plugins/acousticbrainz.rst b/docs/plugins/acousticbrainz.rst new file mode 100644 index 000000000..8e15716a5 --- /dev/null +++ b/docs/plugins/acousticbrainz.rst @@ -0,0 +1,22 @@ +AcousticBrainz Plugin +===================== + +The ``acoustricbrainz`` plugin gets acoustic-analysis information from the +`AcousticBrainz`_ project. The spirit is similar to the +:doc:`/plugins/echonest`. + +.. _AcousticBrainz: http://acousticbrainz.org/ + +Enable the ``acousticbrainz`` plugin in your configuration (see :ref:`using-plugins`) and run it by typing:: + + $ beet acousticbrainz [QUERY] + +For all tracks with a MusicBrainz recording ID, the plugin currently sets +these fields: + +* ``danceable``: Predicts how easy the track is to dance to. +* ``mood_happy``: Predicts the probability this track will evoke happiness. +* ``mood_party``: Predicts the probability this track should be played at a + party. + +These three fields are all numbers between 0.0 and 1.0. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index d1cc41015..7d6313d7f 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -31,6 +31,7 @@ Each plugin has its own set of options that can be defined in a section bearing .. toctree:: :hidden: + acousticbrainz badfiles bpd bpm @@ -95,6 +96,7 @@ Autotagger Extensions Metadata -------- +* :doc:`acousticbrainz`: Fetch various AcousticBrainz metadata * :doc:`bpm`: Measure tempo using keystrokes. * :doc:`echonest`: Automatically fetch `acoustic attributes`_ from `the Echo Nest`_ (tempo, energy, danceability, ...). diff --git a/docs/plugins/play.rst b/docs/plugins/play.rst index 9a7210b4e..5092ce846 100644 --- a/docs/plugins/play.rst +++ b/docs/plugins/play.rst @@ -82,3 +82,17 @@ example:: indicates that you need to insert extra arguments before specifying the playlist. + +Note on the Leakage of the Generated Playlists +_______________________________________________ + +Because the command that will open the generated ``.m3u`` files can be +arbitrarily configured by the user, beets won't try to delete those files. For +this reason, using this plugin will leave one or several playlist(s) in the +directory selected to create temporary files (Most likely ``/tmp/`` on Unix-like +systems. See `tempfile.tempdir`_.). Leaking those playlists until they are +externally wiped could be an issue for privacy or storage reasons. If this is +the case for you, you might want to use the ``raw`` config option described +above. + +.. _tempfile.tempdir: https://docs.python.org/2/library/tempfile.html#tempfile.tempdir diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index f39dad393..5eb440316 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -256,7 +256,7 @@ anywhere in your filesystem. The ``-c`` option copies files instead of moving them. As with other commands, the ``-a`` option matches albums instead of items. To perform a "dry run", just use the ``-p`` (for "pretend") flag. This will -show you all how the files would be moved but won't actually change anything +show you a list of files that would be moved but won't actually change anything on disk. .. _update-cmd: