From 7d6d8f1fd4affc6808fede26a91cf56314d3e1ba Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 21 Jan 2014 16:08:22 +0100 Subject: [PATCH 1/4] Embed item data into web request --- beetsplug/web/__init__.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index dcf383368..ad53c24b5 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -68,10 +68,14 @@ def single_item(item_id): @app.route('/item/') def all_items(): - with g.lib.transaction() as tx: - rows = tx.query("SELECT id FROM items") - all_ids = [row[0] for row in rows] - return flask.jsonify(item_ids=all_ids) + if flask.request.args.has_key('embedded'): + items = [_rep(item) for item in g.lib.items()] + return flask.jsonify(items=items) + else: + with g.lib.transaction() as tx: + rows = tx.query("SELECT id FROM items") + all_ids = [row[0] for row in rows] + return flask.jsonify(item_ids=all_ids) @app.route('/item//file') def item_file(item_id): @@ -97,10 +101,15 @@ def single_album(album_id): @app.route('/album/') def all_albums(): - with g.lib.transaction() as tx: - rows = tx.query("SELECT id FROM albums") - all_ids = [row[0] for row in rows] - return flask.jsonify(album_ids=all_ids) + if flask.request.args.has_key('embedded'): + albums = g.lib.albums() + return flask.jsonify(results=[_rep(album) for album in albums]) + else: + with g.lib.transaction() as tx: + rows = tx.query("SELECT id FROM albums") + all_ids = [row[0] for row in rows] + return flask.jsonify(album_ids=all_ids) + @app.route('/album/query/') def album_query(query): From 9d1731faf2810b320019cfc5e4214302aba781dd Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 21 Jan 2014 16:42:25 +0100 Subject: [PATCH 2/4] Use generators to stream web requests --- beetsplug/web/__init__.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index ad53c24b5..186c3b0d2 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -20,6 +20,7 @@ import beets.library import flask from flask import g import os +import json # Utilities. @@ -49,6 +50,23 @@ def _rep(obj, expand=False): out['items'] = [_rep(item) for item in obj.items()] return out +def json_generator(items, root): + """Generator that dumps list of beets Items or Albums as JSON + + :param root: root key for JSON + :param items: list of :class:`Item` or :class:`Album` to dump + :returns: generator that yields strings + """ + yield '{"%s":[' % root + first = True + for item in items: + if first: + first = False + else: + yield ',' + yield json.dumps(_rep(item)) + yield ']}' + # Flask setup. @@ -69,8 +87,9 @@ def single_item(item_id): @app.route('/item/') def all_items(): if flask.request.args.has_key('embedded'): - items = [_rep(item) for item in g.lib.items()] - return flask.jsonify(items=items) + return app.response_class( + json_generator(g.lib.items(), root='items'), + mimetype='application/json') else: with g.lib.transaction() as tx: rows = tx.query("SELECT id FROM items") @@ -102,8 +121,9 @@ def single_album(album_id): @app.route('/album/') def all_albums(): if flask.request.args.has_key('embedded'): - albums = g.lib.albums() - return flask.jsonify(results=[_rep(album) for album in albums]) + return app.response_class( + json_generator(g.lib.albums(), root='albums'), + mimetype='application/json') else: with g.lib.transaction() as tx: rows = tx.query("SELECT id FROM albums") From 83e86241b8170e33af52f2109b7a5266ecae7fe8 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 21 Jan 2014 22:39:48 +0100 Subject: [PATCH 3/4] Add JSON-API tests --- test/test_web.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/test_web.py diff --git a/test/test_web.py b/test/test_web.py new file mode 100644 index 000000000..d038b410d --- /dev/null +++ b/test/test_web.py @@ -0,0 +1,63 @@ +"""Tests for the 'web' plugin""" + +from _common import unittest +import _common +import json +import beets +import beetsplug +beetsplug.__path__ = ['./beetsplug', '../beetsplug'] +from beetsplug import web + + +class WebPluginTest(_common.LibTestCase): + + def setUp(self): + super(WebPluginTest, self).setUp() + + # Add fixtures + self.lib.add(beets.library.Item( + title = u'another title', + path = 'somepath' + str(_common._item_ident))) + self.lib.add(beets.library.Album()) + self.lib.add(beets.library.Album()) + + web.app.config['TESTING'] = True + web.app.config['lib'] = self.lib + self.client = web.app.test_client() + + def test_get_item(self): + response = self.client.get('/item/') + response.json = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json['items']), 2) + + def test_get_item_empty_query(self): + response = self.client.get('/item/query/') + response.json = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json['items']), 2) + + def test_get_album(self): + response = self.client.get('/album/') + response.json = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json['albums']), 2) + + def test_get_album_empty_query(self): + response = self.client.get('/album/query/') + response.json = json.loads(response.data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json['albums']), 2) + + + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From f98240b83ca1c1a2b7e266f0c3c67e986f42aab5 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 21 Jan 2014 22:40:42 +0100 Subject: [PATCH 4/4] JSON API responds with embedded items This effectively removes the "album_ids" and "item_ids" keys from the responses of `/item/` and `/album/`, respectively. Instead there are "albums" and "items" keys, respectively that contain a list of JSON representations for all albums and items. --- beetsplug/web/__init__.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 186c3b0d2..50a0c63e6 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -85,16 +85,11 @@ def single_item(item_id): return flask.jsonify(_rep(item)) @app.route('/item/') +@app.route('/item/query/') def all_items(): - if flask.request.args.has_key('embedded'): - return app.response_class( - json_generator(g.lib.items(), root='items'), - mimetype='application/json') - else: - with g.lib.transaction() as tx: - rows = tx.query("SELECT id FROM items") - all_ids = [row[0] for row in rows] - return flask.jsonify(item_ids=all_ids) + return app.response_class( + json_generator(g.lib.items(), root='items'), + mimetype='application/json') @app.route('/item//file') def item_file(item_id): @@ -119,16 +114,11 @@ def single_album(album_id): return flask.jsonify(_rep(album)) @app.route('/album/') +@app.route('/album/query/') def all_albums(): - if flask.request.args.has_key('embedded'): - return app.response_class( - json_generator(g.lib.albums(), root='albums'), - mimetype='application/json') - else: - with g.lib.transaction() as tx: - rows = tx.query("SELECT id FROM albums") - all_ids = [row[0] for row in rows] - return flask.jsonify(album_ids=all_ids) + return app.response_class( + json_generator(g.lib.albums(), root='albums'), + mimetype='application/json') @app.route('/album/query/')