Merge pull request #2389 from irskep/pr/web-features

A new 'beet web' endpoint and configuration option, with docs
This commit is contained in:
Adrian Sampson 2017-01-16 08:43:57 -08:00 committed by GitHub
commit 23e05fe0b1
4 changed files with 76 additions and 4 deletions

View file

@ -37,7 +37,10 @@ def _rep(obj, expand=False):
out = dict(obj)
if isinstance(obj, beets.library.Item):
del out['path']
if app.config.get('INCLUDE_PATHS', False):
out['path'] = util.displayable_path(out['path'])
else:
del out['path']
# Get the size (in bytes) of the backing file. This is useful
# for the Tomahawk resolver API.
@ -173,11 +176,16 @@ class QueryConverter(PathConverter):
return ','.join(value)
class EverythingConverter(PathConverter):
regex = '.*?'
# Flask setup.
app = flask.Flask(__name__)
app.url_map.converters['idlist'] = IdListConverter
app.url_map.converters['query'] = QueryConverter
app.url_map.converters['everything'] = EverythingConverter
@app.before_request
@ -218,6 +226,16 @@ def item_query(queries):
return g.lib.items(queries)
@app.route('/item/path/<everything:path>')
def item_at_path(path):
query = beets.library.PathQuery('path', path.encode('utf-8'))
item = g.lib.items(query).get()
if item:
return flask.jsonify(_rep(item))
else:
return flask.abort(404)
@app.route('/item/values/<string:key>')
def item_unique_field_values(key):
sort_key = flask.request.args.get('sort_key', key)
@ -309,6 +327,7 @@ class WebPlugin(BeetsPlugin):
'host': u'127.0.0.1',
'port': 8337,
'cors': '',
'include_paths': False,
})
def commands(self):
@ -327,6 +346,8 @@ class WebPlugin(BeetsPlugin):
# Normalizes json output
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
app.config['INCLUDE_PATHS'] = self.config['include_paths']
# Enable CORS if required.
if self.config['cors']:
self._log.info(u'Enabling CORS with origin: {0}',

View file

@ -13,6 +13,10 @@ New features:
non-numeric track index data. For example, some vinyl or tape media will
report the side of the record using a letter instead of a number in that
field. :bug:`1831` :bug:`2363`
* The :doc:`/plugins/web` has a new endpoint, ``/item/path/foo``, which will
return the item info for the file at the given path, or 404.
* The :doc:`/plugins/web` also has a new config option, ``include_paths``,
which will cause paths to be included in item API responses if set to true.
Fixes:

View file

@ -63,6 +63,8 @@ configuration file. The available options are:
Default: 8337.
- **cors**: The CORS allowed origin (see :ref:`web-cors`, below).
Default: CORS is disabled.
- **include_paths**: If true, includes paths in item objects.
Default: false.
Implementation
--------------
@ -160,6 +162,16 @@ response includes all the items requested. If a track is not found it is silentl
dropped from the response.
``GET /item/path/...``
++++++++++++++++++++++
Look for an item at the given absolute path on the server. If it corresponds to
a track, return the track in the same format as ``/item/*``.
If the server runs UNIX, you'll need to include an extra leading slash:
``http://localhost:8337/item/path//Users/beets/Music/Foo/Bar/Baz.mp3``
``GET /item/query/querystring``
+++++++++++++++++++++++++++++++

View file

@ -4,11 +4,12 @@
from __future__ import division, absolute_import, print_function
import json
import unittest
import os.path
from six import assertCountEqual
from test import _common
import json
from beets.library import Item, Album
from beetsplug import web
@ -21,15 +22,32 @@ class WebPluginTest(_common.LibTestCase):
# Add fixtures
for track in self.lib.items():
track.remove()
self.lib.add(Item(title=u'title', path='', id=1))
self.lib.add(Item(title=u'another title', path='', id=2))
self.lib.add(Item(title=u'title', path='/path_1', id=1))
self.lib.add(Item(title=u'another title', path='/path_2', id=2))
self.lib.add(Album(album=u'album', id=3))
self.lib.add(Album(album=u'another album', id=4))
web.app.config['TESTING'] = True
web.app.config['lib'] = self.lib
web.app.config['INCLUDE_PATHS'] = False
self.client = web.app.test_client()
def test_config_include_paths_true(self):
web.app.config['INCLUDE_PATHS'] = True
response = self.client.get('/item/1')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['path'], u'/path_1')
def test_config_include_paths_false(self):
web.app.config['INCLUDE_PATHS'] = False
response = self.client.get('/item/1')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertNotIn('path', response.json)
def test_get_all_items(self):
response = self.client.get('/item/')
response.json = json.loads(response.data.decode('utf-8'))
@ -58,6 +76,23 @@ class WebPluginTest(_common.LibTestCase):
response = self.client.get('/item/3')
self.assertEqual(response.status_code, 404)
def test_get_single_item_by_path(self):
data_path = os.path.join(_common.RSRC, b'full.mp3')
self.lib.add(Item.from_path(data_path))
response = self.client.get('/item/path/' + data_path.decode('utf-8'))
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['title'], u'full')
def test_get_single_item_by_path_not_found_if_not_in_library(self):
data_path = os.path.join(_common.RSRC, b'full.mp3')
# data_path points to a valid file, but we have not added the file
# to the library.
response = self.client.get('/item/path/' + data_path.decode('utf-8'))
self.assertEqual(response.status_code, 404)
def test_get_item_empty_query(self):
response = self.client.get('/item/query/')
response.json = json.loads(response.data.decode('utf-8'))