Merge branch 'master' into Stunner-master

This commit is contained in:
Adrian Sampson 2017-03-06 23:51:10 -05:00
commit 74df2788c0
16 changed files with 275 additions and 40 deletions

26
beets/__main__.py Normal file
View file

@ -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:])

View file

@ -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:

View file

@ -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.

View file

@ -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):

View file

@ -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:

View file

@ -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)

87
beetsplug/kodiupdate.py Normal file
View file

@ -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.')

View file

@ -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())

View file

@ -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)
)

View file

@ -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)

View file

@ -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 <keywordquery>` 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::

View file

@ -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
<https://www.discogs.com/settings/developers>`_. 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
---------------

View file

@ -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
-------------

View file

@ -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``

View file

@ -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
-------

View file

@ -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():