diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index e7b9ec81f..bd4677bd8 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -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/') +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/') 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}', diff --git a/docs/changelog.rst b/docs/changelog.rst index eaf8c7d5a..be7020dc1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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: diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index f4ae063e0..9f1bbcd99 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -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`` +++++++++++++++++++++++++++++++ diff --git a/test/test_web.py b/test/test_web.py index e72ecf33d..98347d8af 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -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'))