diff --git a/beets/__main__.py b/beets/__main__.py new file mode 100644 index 000000000..8010ca0dd --- /dev/null +++ b/beets/__main__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2017, Adrian Sampson. +# +# 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. + +"""The __main__ module lets you run the beets CLI interface by typing +`python -m beets`. +""" + +from __future__ import division, absolute_import, print_function + +import sys +from .ui import main + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/beets/importer.py b/beets/importer.py index bbe152cd4..690a499ff 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -988,7 +988,7 @@ class ArchiveImportTask(SentinelImportTask): `toppath` to that directory. """ for path_test, handler_class in self.handlers(): - if path_test(self.toppath): + if path_test(util.py3_path(self.toppath)): break try: diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index ae30a9c60..df370b52e 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -126,8 +126,7 @@ def print_(*strings, **kwargs): Python 3. The `end` keyword argument behaves similarly to the built-in `print` - (it defaults to a newline). The value should have the same string - type as the arguments. + (it defaults to a newline). """ if not strings: strings = [u''] @@ -136,11 +135,23 @@ def print_(*strings, **kwargs): txt = u' '.join(strings) txt += kwargs.get('end', u'\n') - # Send bytes to the stdout stream on Python 2. + # Encode the string and write it to stdout. if six.PY2: - txt = txt.encode(_out_encoding(), 'replace') - - sys.stdout.write(txt) + # On Python 2, sys.stdout expects bytes. + out = txt.encode(_out_encoding(), 'replace') + sys.stdout.write(out) + else: + # On Python 3, sys.stdout expects text strings and uses the + # exception-throwing encoding error policy. To avoid throwing + # errors and use our configurable encoding override, we use the + # underlying bytes buffer instead. + if hasattr(sys.stdout, 'buffer'): + out = txt.encode(_out_encoding(), 'replace') + sys.stdout.buffer.write(out) + else: + # In our test harnesses (e.g., DummyOut), sys.stdout.buffer + # does not exist. We instead just record the text string. + sys.stdout.write(txt) # Configuration wrappers. diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index c62abf7ab..8a73efa16 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -161,7 +161,8 @@ class BeatportClient(object): :returns: Tracks in the matching release :rtype: list of :py:class:`BeatportTrack` """ - response = self._get('/catalog/3/tracks', releaseId=beatport_id) + response = self._get('/catalog/3/tracks', releaseId=beatport_id, + perPage=100) return [BeatportTrack(t) for t in response] def get_track(self, beatport_id): diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 3b3f5f201..0957b3403 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -54,9 +54,11 @@ class DiscogsPlugin(BeetsPlugin): 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, + 'user_token': '', }) self.config['apikey'].redact = True self.config['apisecret'].redact = True + self.config['user_token'].redact = True self.discogs_client = None self.register_listener('import_begin', self.setup) @@ -66,6 +68,12 @@ class DiscogsPlugin(BeetsPlugin): c_key = self.config['apikey'].as_str() c_secret = self.config['apisecret'].as_str() + # Try using a configured user token (bypassing OAuth login). + user_token = self.config['user_token'].as_str() + if user_token: + self.discogs_client = Client(USER_AGENT, user_token=user_token) + return + # Get the OAuth token from a file or log in. try: with open(self._tokenfile()) as f: diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 5ca314d3f..93d53c58a 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -21,7 +21,8 @@ import shlex from beets.plugins import BeetsPlugin from beets.ui import decargs, print_, Subcommand, UserError -from beets.util import command_output, displayable_path, subprocess +from beets.util import command_output, displayable_path, subprocess, \ + bytestring_path from beets.library import Item, Album import six @@ -112,14 +113,14 @@ class DuplicatesPlugin(BeetsPlugin): self.config.set_args(opts) album = self.config['album'].get(bool) checksum = self.config['checksum'].get(str) - copy = self.config['copy'].get(str) + copy = bytestring_path(self.config['copy'].as_str()) count = self.config['count'].get(bool) delete = self.config['delete'].get(bool) fmt = self.config['format'].get(str) full = self.config['full'].get(bool) keys = self.config['keys'].as_str_seq() merge = self.config['merge'].get(bool) - move = self.config['move'].get(str) + move = bytestring_path(self.config['move'].as_str()) path = self.config['path'].get(bool) tiebreak = self.config['tiebreak'].get(dict) strict = self.config['strict'].get(bool) diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py new file mode 100644 index 000000000..78f120d89 --- /dev/null +++ b/beetsplug/kodiupdate.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2017, Pauli Kettunen. +# +# 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. + +"""Updates a Kodi library whenever the beets library is changed. +This is based on the Plex Update plugin. + +Put something like the following in your config.yaml to configure: + kodi: + host: localhost + port: 8080 + user: user + pwd: secret +""" +from __future__ import division, absolute_import, print_function + +import requests +from beets import config +from beets.plugins import BeetsPlugin + + +def update_kodi(host, port, user, password): + """Sends request to the Kodi api to start a library refresh. + """ + url = "http://{0}:{1}/jsonrpc/".format(host, port) + + """Content-Type: application/json is mandatory + according to the kodi jsonrpc documentation""" + + headers = {'Content-Type': 'application/json'} + + # Create the payload. Id seems to be mandatory. + payload = {'jsonrpc': '2.0', 'method': 'AudioLibrary.Scan', 'id': 1} + r = requests.post( + url, + auth=(user, password), + json=payload, + headers=headers) + + return r + + +class KodiUpdate(BeetsPlugin): + def __init__(self): + super(KodiUpdate, self).__init__() + + # Adding defaults. + config['kodi'].add({ + u'host': u'localhost', + u'port': 8080, + u'user': u'kodi', + u'pwd': u'kodi'}) + + config['kodi']['pwd'].redact = True + self.register_listener('database_change', self.listen_for_db_change) + + def listen_for_db_change(self, lib, model): + """Listens for beets db change and register the update""" + self.register_listener('cli_exit', self.update) + + def update(self, lib): + """When the client exists try to send refresh request to Kodi server. + """ + self._log.info(u'Updating Kodi library...') + + # Try to send update request. + try: + update_kodi( + config['kodi']['host'].get(), + config['kodi']['port'].get(), + config['kodi']['user'].get(), + config['kodi']['pwd'].get()) + self._log.info(u'... started.') + + except requests.exceptions.RequestException: + self._log.warning(u'Update failed.') diff --git a/beetsplug/mbsubmit.py b/beetsplug/mbsubmit.py index 58b357dd3..02bd5f697 100644 --- a/beetsplug/mbsubmit.py +++ b/beetsplug/mbsubmit.py @@ -56,5 +56,5 @@ class MBSubmitPlugin(BeetsPlugin): return [PromptChoice(u'p', u'Print tracks', self.print_tracks)] def print_tracks(self, session, task): - for i in task.items: + for i in sorted(task.items, key=lambda i: i.track): print_data(None, i, self.config['format'].as_str()) diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index 1ea90f01e..838206156 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -289,4 +289,10 @@ class GioURI(URIGetter): raise finally: self.libgio.g_free(uri_ptr) - return uri + + try: + return uri.decode(util._fsencoding()) + except UnicodeDecodeError: + raise RuntimeError( + "Could not decode filename from GIO: {!r}".format(uri) + ) diff --git a/docs/changelog.rst b/docs/changelog.rst index 236d174ab..fd37f992e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,8 +31,11 @@ New features: * A new :ref:`hardlink` config option instructs the importer to create hard links on filesystems that support them. Thanks to :user:`jacobwgillespie`. :bug:`2445` -* :doc:`/plugins/embedart` by default now asks for confirmation before +* :doc:`/plugins/embedart` by default now asks for confirmation before embedding art into music files. Thanks to :user:`Stunner`. :bug:`1999` +* You can now run beets by typing `python -m beets`. :bug:`2453` +* A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync + with beets. Thanks to :user:`Pauligrinder`. :bug:`2411` Fixes: @@ -50,6 +53,21 @@ Fixes: command is not found or exists with an error. :bug:`2430` :bug:`2433` * :doc:`/plugins/lyrics`: The Google search backend no longer crashes when the server responds with an error. :bug:`2437` +* :doc:`/plugins/discogs`: You can now authenticate with Discogs using a + personal access token. :bug:`2447` +* Fix Python 3 compatibility when extracting rar archives in the importer. + Thanks to :user:`Lompik`. :bug:`2443` :bug:`2448` +* :doc:`/plugins/duplicates`: Fix Python 3 compatibility when using the + ``copy`` and ``move`` options. :bug:`2444` +* :doc:`/plugins/mbsubmit`: The tracks are now sorted. Thanks to + :user:`awesomer`. :bug:`2457` +* :doc:`/plugins/thumbnails`: Fix a string-related crash on Python 3. + :bug:`2466` +* :doc:`/plugins/beatport`: More than just 10 songs are now fetched per album. + :bug:`2469` +* On Python 3, the :ref:`terminal_encoding` setting is respected again for + output and printing will no longer crash on systems configured with a + limited encoding. 1.4.3 (January 9, 2017) diff --git a/docs/guides/main.rst b/docs/guides/main.rst index 7feacb6bf..f0b3635c2 100644 --- a/docs/guides/main.rst +++ b/docs/guides/main.rst @@ -82,7 +82,7 @@ into this if you've installed Python yourself with `Homebrew`_ or otherwise.) If this happens, you can install beets for the current user only (sans ``sudo``) by typing ``pip install --user beets``. If you do that, you might want -to add ``~/Library/Python/2.7/bin`` to your ``$PATH``. +to add ``~/Library/Python/3.6/bin`` to your ``$PATH``. .. _System Integrity Protection: https://support.apple.com/en-us/HT204899 .. _Homebrew: http://brew.sh @@ -93,28 +93,28 @@ Installing on Windows Installing beets on Windows can be tricky. Following these steps might help you get it right: -1. If you don't have it, `install Python`_ (you want Python 2.7). +1. If you don't have it, `install Python`_ (you want Python 3.6). The + installer should give you the option to "add Python to PATH." Check this + box. If you do that, you can skip the next step. 2. If you haven't done so already, set your ``PATH`` environment variable to include Python and its scripts. To do so, you have to get the "Properties" window for "My Computer", then choose the "Advanced" tab, then hit the "Environment Variables" button, and then look for the ``PATH`` variable in the table. Add the following to the end of the variable's value: - ``;C:\Python27;C:\Python27\Scripts``. + ``;C:\Python36;C:\Python36\Scripts``. You may need to adjust these paths to + point to your Python installation. -3. Next, `install pip`_ (if you don't have it already) by downloading and - running the `get-pip.py`_ script. +3. Now install beets by running: ``pip install beets`` -4. Now install beets by running: ``pip install beets`` - -5. You're all set! Type ``beet`` at the command prompt to make sure everything's +4. You're all set! Type ``beet`` at the command prompt to make sure everything's in order. Windows users may also want to install a context menu item for importing files -into beets. Just download and open `beets.reg`_ to add the necessary keys to the -registry. You can then right-click a directory and choose "Import with beets". -If Python is in a nonstandard location on your system, you may have to edit the -command path manually. +into beets. Download the `beets.reg`_ file and open it in a text file to make +sure the paths to Python match your system. Then double-click the file add the +necessary keys to your registry. You can then right-click a directory and +choose "Import with beets". Because I don't use Windows myself, I may have missed something. If you have trouble or you have more detail to contribute here, please direct it to @@ -142,8 +142,8 @@ place to start:: Change that first path to a directory where you'd like to keep your music. Then, for ``library``, choose a good place to keep a database file that keeps an index of your music. (The config's format is `YAML`_. You'll want to configure your -text editor to use spaces, not real tabs, for indentation.) - +text editor to use spaces, not real tabs, for indentation. Also, ``~`` means +your home directory in these paths, even on Windows.) The default configuration assumes you want to start a new organized music folder (that ``directory`` above) and that you'll *copy* cleaned-up music into that @@ -238,21 +238,30 @@ songs. Thus:: $ beet ls album:bird The Mae Shi - Terrorbird - Revelation Six -As you can see, search terms by default search all attributes of songs. (They're +By default, a search term will match any of a handful of :ref:`common +attributes ` of songs. +(They're also implicitly joined by ANDs: a track must match *all* criteria in order to match the query.) To narrow a search term to a particular metadata field, just put the field before the term, separated by a : character. So ``album:bird`` only looks for ``bird`` in the "album" field of your songs. (Need to know more? :doc:`/reference/query/` will answer all your questions.) -The ``beet list`` command has another useful option worth mentioning, ``-a``, -which searches for albums instead of songs:: +The ``beet list`` command also has an ``-a`` option, which searches for albums instead of songs:: $ beet ls -a forever Bon Iver - For Emma, Forever Ago Freezepop - Freezepop Forever -So handy! +There's also an ``-f`` option (for *format*) that lets you specify what gets displayed in the results of a search:: + + $ beet ls -a forever -f "[$format] $album ($year) - $artist - $title" + [MP3] For Emma, Forever Ago (2009) - Bon Iver - Flume + [AAC] Freezepop Forever (2011) - Freezepop - Harebrained Scheme + +In the format option, field references like `$format` and `$year` are filled +in with data from each result. You can see a full list of available fields by +running ``beet fields``. Beets also has a ``stats`` command, just in case you want to see how much music you have:: diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index eb60cca58..a02b34590 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -14,10 +14,9 @@ To use the ``discogs`` plugin, first enable it in your configuration (see pip install discogs-client -You will also need to register for a `Discogs`_ account. The first time you -run the :ref:`import-cmd` command after enabling the plugin, it will ask you -to authorize with Discogs by visiting the site in a browser. Subsequent runs -will not require re-authorization. +You will also need to register for a `Discogs`_ account, and provide +authentication credentials via a personal access token or an OAuth2 +authorization. Matches from Discogs will now show up during import alongside matches from MusicBrainz. @@ -25,6 +24,25 @@ MusicBrainz. If you have a Discogs ID for an album you want to tag, you can also enter it at the "enter Id" prompt in the importer. +OAuth Authorization +``````````````````` + +The first time you run the :ref:`import-cmd` command after enabling the plugin, +it will ask you to authorize with Discogs by visiting the site in a browser. +Subsequent runs will not require re-authorization. + +Authentication via Personal Access Token +```````````````````````````````````````` + +As an alternative to OAuth, you can get a token from Discogs and add it to +your configuration. +To get a personal access token (called a "user token" in the `discogs-client`_ +documentation), login to `Discogs`_, and visit the +`Developer settings page +`_. Press the ``Generate new +token`` button, and place the generated token in your configuration, as the +``user_token`` config option in the ``discogs`` section. + Troubleshooting --------------- diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 0e851d7da..eaeec02aa 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -66,6 +66,7 @@ like this:: inline ipfs keyfinder + kodiupdate lastgenre lastimport lyrics @@ -148,6 +149,8 @@ Interoperability * :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes. * :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. * :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs. +* :doc:`kodiupdate`: Automatically notifies `Kodi`_ whenever the beets library + changes. * :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library changes. * :doc:`play`: Play beets queries in your music player. @@ -160,6 +163,7 @@ Interoperability .. _Emby: http://emby.media .. _Plex: http://plex.tv +.. _Kodi: http://kodi.tv Miscellaneous ------------- diff --git a/docs/plugins/kodiupdate.rst b/docs/plugins/kodiupdate.rst new file mode 100644 index 000000000..a1ec04775 --- /dev/null +++ b/docs/plugins/kodiupdate.rst @@ -0,0 +1,44 @@ +KodiUpdate Plugin +================= + +The ``kodiupdate`` plugin lets you automatically update `Kodi`_'s music +library whenever you change your beets library. + +To use ``kodiupdate`` plugin, enable it in your configuration +(see :ref:`using-plugins`). +Then, you'll want to configure the specifics of your Kodi host. +You can do that using a ``kodi:`` section in your ``config.yaml``, +which looks like this:: + + kodi: + host: localhost + port: 8080 + user: kodi + pwd: kodi + +To use the ``kodiupdate`` plugin you need to install the `requests`_ library with:: + + pip install requests + +You'll also need to enable JSON-RPC in Kodi in order the use the plugin. +In Kodi's interface, navigate to System/Settings/Network/Services and choose "Allow control of Kodi via HTTP." + +With that all in place, you'll see beets send the "update" command to your Kodi +host every time you change your beets library. + +.. _Kodi: http://kodi.tv/ +.. _requests: http://docs.python-requests.org/en/latest/ + +Configuration +------------- + +The available options under the ``kodi:`` section are: + +- **host**: The Kodi host name. + Default: ``localhost`` +- **port**: The Kodi host port. + Default: 8080 +- **user**: The Kodi host user. + Default: ``kodi`` +- **pwd**: The Kodi host password. + Default: ``kodi`` diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 2f3366d4c..b4789aa10 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -6,6 +6,8 @@ searches that select tracks and albums from your library. This page explains the query string syntax, which is meant to vaguely resemble the syntax used by Web search engines. +.. _keywordquery: + Keyword ------- diff --git a/test/test_thumbnails.py b/test/test_thumbnails.py index d287c073a..dc03f06f7 100644 --- a/test/test_thumbnails.py +++ b/test/test_thumbnails.py @@ -276,12 +276,12 @@ class ThumbnailsTest(unittest.TestCase, TestHelper): if not gio.available: self.skipTest(u"GIO library not found") - self.assertEqual(gio.uri(u"/foo"), b"file:///") # silent fail - self.assertEqual(gio.uri(b"/foo"), b"file:///foo") - self.assertEqual(gio.uri(b"/foo!"), b"file:///foo!") + self.assertEqual(gio.uri(u"/foo"), u"file:///") # silent fail + self.assertEqual(gio.uri(b"/foo"), u"file:///foo") + self.assertEqual(gio.uri(b"/foo!"), u"file:///foo!") self.assertEqual( gio.uri(b'/music/\xec\x8b\xb8\xec\x9d\xb4'), - b'file:///music/%EC%8B%B8%EC%9D%B4') + u'file:///music/%EC%8B%B8%EC%9D%B4') def suite():