We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Slowdown with beet web - regression
There is a massive slowdown in queries with python2 and python3 when using the beet web interface. 

When large queries are run, i.e. 'format:flac' on a flac only library, the web interface posts queries 10x slower than before the regression.  To clarify that does seem to be a relative time based on the length of time originally taken by the query. 

This is caused by a regression in the commit. 
https://github.com/beetbox/beets/commit/5e8ac9e4a5d06de791fe051a419ba070bbdd5bec

beet stats 
Tracks: 43913
Total time: 51.4 weeks
Approximate total size: 976.18 GiB
Artists: 7345
Albums: 12004
Album artists: 1800


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `beetsplug/web/__init__.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # This file is part of beets.
3 # Copyright 2016, Adrian Sampson.
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 
16 """A Web interface to beets."""
17 from __future__ import division, absolute_import, print_function
18 
19 from beets.plugins import BeetsPlugin
20 from beets import ui
21 from beets import util
22 import beets.library
23 import flask
24 from flask import g
25 from werkzeug.routing import BaseConverter, PathConverter
26 import os
27 import json
28 
29 
30 # Utilities.
31 
32 def _rep(obj, expand=False):
33     """Get a flat -- i.e., JSON-ish -- representation of a beets Item or
34     Album object. For Albums, `expand` dictates whether tracks are
35     included.
36     """
37     out = dict(obj)
38 
39     if isinstance(obj, beets.library.Item):
40         out['path'] = obj.destination(fragment=True)
41 
42         # Get the size (in bytes) of the backing file. This is useful
43         # for the Tomahawk resolver API.
44         try:
45             out['size'] = os.path.getsize(util.syspath(obj.path))
46         except OSError:
47             out['size'] = 0
48 
49         return out
50 
51     elif isinstance(obj, beets.library.Album):
52         del out['artpath']
53         if expand:
54             out['items'] = [_rep(item) for item in obj.items()]
55         return out
56 
57 
58 def json_generator(items, root, expand=False):
59     """Generator that dumps list of beets Items or Albums as JSON
60 
61     :param root:  root key for JSON
62     :param items: list of :class:`Item` or :class:`Album` to dump
63     :param expand: If true every :class:`Album` contains its items in the json
64                    representation
65     :returns:     generator that yields strings
66     """
67     yield '{"%s":[' % root
68     first = True
69     for item in items:
70         if first:
71             first = False
72         else:
73             yield ','
74         yield json.dumps(_rep(item, expand=expand))
75     yield ']}'
76 
77 
78 def is_expand():
79     """Returns whether the current request is for an expanded response."""
80 
81     return flask.request.args.get('expand') is not None
82 
83 
84 def resource(name):
85     """Decorates a function to handle RESTful HTTP requests for a resource.
86     """
87     def make_responder(retriever):
88         def responder(ids):
89             entities = [retriever(id) for id in ids]
90             entities = [entity for entity in entities if entity]
91 
92             if len(entities) == 1:
93                 return flask.jsonify(_rep(entities[0], expand=is_expand()))
94             elif entities:
95                 return app.response_class(
96                     json_generator(entities, root=name),
97                     mimetype='application/json'
98                 )
99             else:
100                 return flask.abort(404)
101         responder.__name__ = 'get_{0}'.format(name)
102         return responder
103     return make_responder
104 
105 
106 def resource_query(name):
107     """Decorates a function to handle RESTful HTTP queries for resources.
108     """
109     def make_responder(query_func):
110         def responder(queries):
111             return app.response_class(
112                 json_generator(
113                     query_func(queries),
114                     root='results', expand=is_expand()
115                 ),
116                 mimetype='application/json'
117             )
118         responder.__name__ = 'query_{0}'.format(name)
119         return responder
120     return make_responder
121 
122 
123 def resource_list(name):
124     """Decorates a function to handle RESTful HTTP request for a list of
125     resources.
126     """
127     def make_responder(list_all):
128         def responder():
129             return app.response_class(
130                 json_generator(list_all(), root=name, expand=is_expand()),
131                 mimetype='application/json'
132             )
133         responder.__name__ = 'all_{0}'.format(name)
134         return responder
135     return make_responder
136 
137 
138 def _get_unique_table_field_values(model, field, sort_field):
139     """ retrieve all unique values belonging to a key from a model """
140     if field not in model.all_keys() or sort_field not in model.all_keys():
141         raise KeyError
142     with g.lib.transaction() as tx:
143         rows = tx.query('SELECT DISTINCT "{0}" FROM "{1}" ORDER BY "{2}"'
144                         .format(field, model._table, sort_field))
145     return [row[0] for row in rows]
146 
147 
148 class IdListConverter(BaseConverter):
149     """Converts comma separated lists of ids in urls to integer lists.
150     """
151 
152     def to_python(self, value):
153         ids = []
154         for id in value.split(','):
155             try:
156                 ids.append(int(id))
157             except ValueError:
158                 pass
159         return ids
160 
161     def to_url(self, value):
162         return ','.join(value)
163 
164 
165 class QueryConverter(PathConverter):
166     """Converts slash separated lists of queries in the url to string list.
167     """
168 
169     def to_python(self, value):
170         return value.split('/')
171 
172     def to_url(self, value):
173         return ','.join(value)
174 
175 
176 # Flask setup.
177 
178 app = flask.Flask(__name__)
179 app.url_map.converters['idlist'] = IdListConverter
180 app.url_map.converters['query'] = QueryConverter
181 
182 
183 @app.before_request
184 def before_request():
185     g.lib = app.config['lib']
186 
187 
188 # Items.
189 
190 @app.route('/item/<idlist:ids>')
191 @resource('items')
192 def get_item(id):
193     return g.lib.get_item(id)
194 
195 
196 @app.route('/item/')
197 @app.route('/item/query/')
198 @resource_list('items')
199 def all_items():
200     return g.lib.items()
201 
202 
203 @app.route('/item/<int:item_id>/file')
204 def item_file(item_id):
205     item = g.lib.get_item(item_id)
206     response = flask.send_file(
207         util.py3_path(item.path),
208         as_attachment=True,
209         attachment_filename=os.path.basename(item.path),
210     )
211     response.headers['Content-Length'] = os.path.getsize(item.path)
212     return response
213 
214 
215 @app.route('/item/query/<query:queries>')
216 @resource_query('items')
217 def item_query(queries):
218     return g.lib.items(queries)
219 
220 
221 @app.route('/item/values/<string:key>')
222 def item_unique_field_values(key):
223     sort_key = flask.request.args.get('sort_key', key)
224     try:
225         values = _get_unique_table_field_values(beets.library.Item, key,
226                                                 sort_key)
227     except KeyError:
228         return flask.abort(404)
229     return flask.jsonify(values=values)
230 
231 
232 # Albums.
233 
234 @app.route('/album/<idlist:ids>')
235 @resource('albums')
236 def get_album(id):
237     return g.lib.get_album(id)
238 
239 
240 @app.route('/album/')
241 @app.route('/album/query/')
242 @resource_list('albums')
243 def all_albums():
244     return g.lib.albums()
245 
246 
247 @app.route('/album/query/<query:queries>')
248 @resource_query('albums')
249 def album_query(queries):
250     return g.lib.albums(queries)
251 
252 
253 @app.route('/album/<int:album_id>/art')
254 def album_art(album_id):
255     album = g.lib.get_album(album_id)
256     if album.artpath:
257         return flask.send_file(album.artpath)
258     else:
259         return flask.abort(404)
260 
261 
262 @app.route('/album/values/<string:key>')
263 def album_unique_field_values(key):
264     sort_key = flask.request.args.get('sort_key', key)
265     try:
266         values = _get_unique_table_field_values(beets.library.Album, key,
267                                                 sort_key)
268     except KeyError:
269         return flask.abort(404)
270     return flask.jsonify(values=values)
271 
272 
273 # Artists.
274 
275 @app.route('/artist/')
276 def all_artists():
277     with g.lib.transaction() as tx:
278         rows = tx.query("SELECT DISTINCT albumartist FROM albums")
279     all_artists = [row[0] for row in rows]
280     return flask.jsonify(artist_names=all_artists)
281 
282 
283 # Library information.
284 
285 @app.route('/stats')
286 def stats():
287     with g.lib.transaction() as tx:
288         item_rows = tx.query("SELECT COUNT(*) FROM items")
289         album_rows = tx.query("SELECT COUNT(*) FROM albums")
290     return flask.jsonify({
291         'items': item_rows[0][0],
292         'albums': album_rows[0][0],
293     })
294 
295 
296 # UI.
297 
298 @app.route('/')
299 def home():
300     return flask.render_template('index.html')
301 
302 
303 # Plugin hook.
304 
305 class WebPlugin(BeetsPlugin):
306     def __init__(self):
307         super(WebPlugin, self).__init__()
308         self.config.add({
309             'host': u'127.0.0.1',
310             'port': 8337,
311             'cors': '',
312         })
313 
314     def commands(self):
315         cmd = ui.Subcommand('web', help=u'start a Web interface')
316         cmd.parser.add_option(u'-d', u'--debug', action='store_true',
317                               default=False, help=u'debug mode')
318 
319         def func(lib, opts, args):
320             args = ui.decargs(args)
321             if args:
322                 self.config['host'] = args.pop(0)
323             if args:
324                 self.config['port'] = int(args.pop(0))
325 
326             app.config['lib'] = lib
327             # Normalizes json output
328             app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
329 
330             # Enable CORS if required.
331             if self.config['cors']:
332                 self._log.info(u'Enabling CORS with origin: {0}',
333                                self.config['cors'])
334                 from flask.ext.cors import CORS
335                 app.config['CORS_ALLOW_HEADERS'] = "Content-Type"
336                 app.config['CORS_RESOURCES'] = {
337                     r"/*": {"origins": self.config['cors'].get(str)}
338                 }
339                 CORS(app)
340             # Start the web application.
341             app.run(host=self.config['host'].as_str(),
342                     port=self.config['port'].get(int),
343                     debug=opts.debug, threaded=True)
344         cmd.func = func
345         return [cmd]
346
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py
--- a/beetsplug/web/__init__.py
+++ b/beetsplug/web/__init__.py
@@ -37,7 +37,7 @@
     out = dict(obj)
 
     if isinstance(obj, beets.library.Item):
-        out['path'] = obj.destination(fragment=True)
+        del out['path']
 
         # Get the size (in bytes) of the backing file. This is useful
         # for the Tomahawk resolver API.

{"golden_diff": "diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py\n--- a/beetsplug/web/__init__.py\n+++ b/beetsplug/web/__init__.py\n@@ -37,7 +37,7 @@\n     out = dict(obj)\n \n     if isinstance(obj, beets.library.Item):\n-        out['path'] = obj.destination(fragment=True)\n+        del out['path']\n \n         # Get the size (in bytes) of the backing file. This is useful\n         # for the Tomahawk resolver API.\n", "issue": "Slowdown with beet web - regression\nThere is a massive slowdown in queries with python2 and python3 when using the beet web interface. \n\nWhen large queries are run, i.e. 'format:flac' on a flac only library, the web interface posts queries 10x slower than before the regression.  To clarify that does seem to be a relative time based on the length of time originally taken by the query. \n\nThis is caused by a regression in the commit. \nhttps://github.com/beetbox/beets/commit/5e8ac9e4a5d06de791fe051a419ba070bbdd5bec\n\nbeet stats \nTracks: 43913\nTotal time: 51.4 weeks\nApproximate total size: 976.18 GiB\nArtists: 7345\nAlbums: 12004\nAlbum artists: 1800\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A Web interface to beets.\"\"\"\nfrom __future__ import division, absolute_import, print_function\n\nfrom beets.plugins import BeetsPlugin\nfrom beets import ui\nfrom beets import util\nimport beets.library\nimport flask\nfrom flask import g\nfrom werkzeug.routing import BaseConverter, PathConverter\nimport os\nimport json\n\n\n# Utilities.\n\ndef _rep(obj, expand=False):\n    \"\"\"Get a flat -- i.e., JSON-ish -- representation of a beets Item or\n    Album object. For Albums, `expand` dictates whether tracks are\n    included.\n    \"\"\"\n    out = dict(obj)\n\n    if isinstance(obj, beets.library.Item):\n        out['path'] = obj.destination(fragment=True)\n\n        # Get the size (in bytes) of the backing file. This is useful\n        # for the Tomahawk resolver API.\n        try:\n            out['size'] = os.path.getsize(util.syspath(obj.path))\n        except OSError:\n            out['size'] = 0\n\n        return out\n\n    elif isinstance(obj, beets.library.Album):\n        del out['artpath']\n        if expand:\n            out['items'] = [_rep(item) for item in obj.items()]\n        return out\n\n\ndef json_generator(items, root, expand=False):\n    \"\"\"Generator that dumps list of beets Items or Albums as JSON\n\n    :param root:  root key for JSON\n    :param items: list of :class:`Item` or :class:`Album` to dump\n    :param expand: If true every :class:`Album` contains its items in the json\n                   representation\n    :returns:     generator that yields strings\n    \"\"\"\n    yield '{\"%s\":[' % root\n    first = True\n    for item in items:\n        if first:\n            first = False\n        else:\n            yield ','\n        yield json.dumps(_rep(item, expand=expand))\n    yield ']}'\n\n\ndef is_expand():\n    \"\"\"Returns whether the current request is for an expanded response.\"\"\"\n\n    return flask.request.args.get('expand') is not None\n\n\ndef resource(name):\n    \"\"\"Decorates a function to handle RESTful HTTP requests for a resource.\n    \"\"\"\n    def make_responder(retriever):\n        def responder(ids):\n            entities = [retriever(id) for id in ids]\n            entities = [entity for entity in entities if entity]\n\n            if len(entities) == 1:\n                return flask.jsonify(_rep(entities[0], expand=is_expand()))\n            elif entities:\n                return app.response_class(\n                    json_generator(entities, root=name),\n                    mimetype='application/json'\n                )\n            else:\n                return flask.abort(404)\n        responder.__name__ = 'get_{0}'.format(name)\n        return responder\n    return make_responder\n\n\ndef resource_query(name):\n    \"\"\"Decorates a function to handle RESTful HTTP queries for resources.\n    \"\"\"\n    def make_responder(query_func):\n        def responder(queries):\n            return app.response_class(\n                json_generator(\n                    query_func(queries),\n                    root='results', expand=is_expand()\n                ),\n                mimetype='application/json'\n            )\n        responder.__name__ = 'query_{0}'.format(name)\n        return responder\n    return make_responder\n\n\ndef resource_list(name):\n    \"\"\"Decorates a function to handle RESTful HTTP request for a list of\n    resources.\n    \"\"\"\n    def make_responder(list_all):\n        def responder():\n            return app.response_class(\n                json_generator(list_all(), root=name, expand=is_expand()),\n                mimetype='application/json'\n            )\n        responder.__name__ = 'all_{0}'.format(name)\n        return responder\n    return make_responder\n\n\ndef _get_unique_table_field_values(model, field, sort_field):\n    \"\"\" retrieve all unique values belonging to a key from a model \"\"\"\n    if field not in model.all_keys() or sort_field not in model.all_keys():\n        raise KeyError\n    with g.lib.transaction() as tx:\n        rows = tx.query('SELECT DISTINCT \"{0}\" FROM \"{1}\" ORDER BY \"{2}\"'\n                        .format(field, model._table, sort_field))\n    return [row[0] for row in rows]\n\n\nclass IdListConverter(BaseConverter):\n    \"\"\"Converts comma separated lists of ids in urls to integer lists.\n    \"\"\"\n\n    def to_python(self, value):\n        ids = []\n        for id in value.split(','):\n            try:\n                ids.append(int(id))\n            except ValueError:\n                pass\n        return ids\n\n    def to_url(self, value):\n        return ','.join(value)\n\n\nclass QueryConverter(PathConverter):\n    \"\"\"Converts slash separated lists of queries in the url to string list.\n    \"\"\"\n\n    def to_python(self, value):\n        return value.split('/')\n\n    def to_url(self, value):\n        return ','.join(value)\n\n\n# Flask setup.\n\napp = flask.Flask(__name__)\napp.url_map.converters['idlist'] = IdListConverter\napp.url_map.converters['query'] = QueryConverter\n\n\n@app.before_request\ndef before_request():\n    g.lib = app.config['lib']\n\n\n# Items.\n\n@app.route('/item/<idlist:ids>')\n@resource('items')\ndef get_item(id):\n    return g.lib.get_item(id)\n\n\n@app.route('/item/')\n@app.route('/item/query/')\n@resource_list('items')\ndef all_items():\n    return g.lib.items()\n\n\n@app.route('/item/<int:item_id>/file')\ndef item_file(item_id):\n    item = g.lib.get_item(item_id)\n    response = flask.send_file(\n        util.py3_path(item.path),\n        as_attachment=True,\n        attachment_filename=os.path.basename(item.path),\n    )\n    response.headers['Content-Length'] = os.path.getsize(item.path)\n    return response\n\n\n@app.route('/item/query/<query:queries>')\n@resource_query('items')\ndef item_query(queries):\n    return g.lib.items(queries)\n\n\n@app.route('/item/values/<string:key>')\ndef item_unique_field_values(key):\n    sort_key = flask.request.args.get('sort_key', key)\n    try:\n        values = _get_unique_table_field_values(beets.library.Item, key,\n                                                sort_key)\n    except KeyError:\n        return flask.abort(404)\n    return flask.jsonify(values=values)\n\n\n# Albums.\n\n@app.route('/album/<idlist:ids>')\n@resource('albums')\ndef get_album(id):\n    return g.lib.get_album(id)\n\n\n@app.route('/album/')\n@app.route('/album/query/')\n@resource_list('albums')\ndef all_albums():\n    return g.lib.albums()\n\n\n@app.route('/album/query/<query:queries>')\n@resource_query('albums')\ndef album_query(queries):\n    return g.lib.albums(queries)\n\n\n@app.route('/album/<int:album_id>/art')\ndef album_art(album_id):\n    album = g.lib.get_album(album_id)\n    if album.artpath:\n        return flask.send_file(album.artpath)\n    else:\n        return flask.abort(404)\n\n\n@app.route('/album/values/<string:key>')\ndef album_unique_field_values(key):\n    sort_key = flask.request.args.get('sort_key', key)\n    try:\n        values = _get_unique_table_field_values(beets.library.Album, key,\n                                                sort_key)\n    except KeyError:\n        return flask.abort(404)\n    return flask.jsonify(values=values)\n\n\n# Artists.\n\n@app.route('/artist/')\ndef all_artists():\n    with g.lib.transaction() as tx:\n        rows = tx.query(\"SELECT DISTINCT albumartist FROM albums\")\n    all_artists = [row[0] for row in rows]\n    return flask.jsonify(artist_names=all_artists)\n\n\n# Library information.\n\n@app.route('/stats')\ndef stats():\n    with g.lib.transaction() as tx:\n        item_rows = tx.query(\"SELECT COUNT(*) FROM items\")\n        album_rows = tx.query(\"SELECT COUNT(*) FROM albums\")\n    return flask.jsonify({\n        'items': item_rows[0][0],\n        'albums': album_rows[0][0],\n    })\n\n\n# UI.\n\n@app.route('/')\ndef home():\n    return flask.render_template('index.html')\n\n\n# Plugin hook.\n\nclass WebPlugin(BeetsPlugin):\n    def __init__(self):\n        super(WebPlugin, self).__init__()\n        self.config.add({\n            'host': u'127.0.0.1',\n            'port': 8337,\n            'cors': '',\n        })\n\n    def commands(self):\n        cmd = ui.Subcommand('web', help=u'start a Web interface')\n        cmd.parser.add_option(u'-d', u'--debug', action='store_true',\n                              default=False, help=u'debug mode')\n\n        def func(lib, opts, args):\n            args = ui.decargs(args)\n            if args:\n                self.config['host'] = args.pop(0)\n            if args:\n                self.config['port'] = int(args.pop(0))\n\n            app.config['lib'] = lib\n            # Normalizes json output\n            app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False\n\n            # Enable CORS if required.\n            if self.config['cors']:\n                self._log.info(u'Enabling CORS with origin: {0}',\n                               self.config['cors'])\n                from flask.ext.cors import CORS\n                app.config['CORS_ALLOW_HEADERS'] = \"Content-Type\"\n                app.config['CORS_RESOURCES'] = {\n                    r\"/*\": {\"origins\": self.config['cors'].get(str)}\n                }\n                CORS(app)\n            # Start the web application.\n            app.run(host=self.config['host'].as_str(),\n                    port=self.config['port'].get(int),\n                    debug=opts.debug, threaded=True)\n        cmd.func = func\n        return [cmd]\n", "path": "beetsplug/web/__init__.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A Web interface to beets.\"\"\"\nfrom __future__ import division, absolute_import, print_function\n\nfrom beets.plugins import BeetsPlugin\nfrom beets import ui\nfrom beets import util\nimport beets.library\nimport flask\nfrom flask import g\nfrom werkzeug.routing import BaseConverter, PathConverter\nimport os\nimport json\n\n\n# Utilities.\n\ndef _rep(obj, expand=False):\n    \"\"\"Get a flat -- i.e., JSON-ish -- representation of a beets Item or\n    Album object. For Albums, `expand` dictates whether tracks are\n    included.\n    \"\"\"\n    out = dict(obj)\n\n    if isinstance(obj, beets.library.Item):\n        del out['path']\n\n        # Get the size (in bytes) of the backing file. This is useful\n        # for the Tomahawk resolver API.\n        try:\n            out['size'] = os.path.getsize(util.syspath(obj.path))\n        except OSError:\n            out['size'] = 0\n\n        return out\n\n    elif isinstance(obj, beets.library.Album):\n        del out['artpath']\n        if expand:\n            out['items'] = [_rep(item) for item in obj.items()]\n        return out\n\n\ndef json_generator(items, root, expand=False):\n    \"\"\"Generator that dumps list of beets Items or Albums as JSON\n\n    :param root:  root key for JSON\n    :param items: list of :class:`Item` or :class:`Album` to dump\n    :param expand: If true every :class:`Album` contains its items in the json\n                   representation\n    :returns:     generator that yields strings\n    \"\"\"\n    yield '{\"%s\":[' % root\n    first = True\n    for item in items:\n        if first:\n            first = False\n        else:\n            yield ','\n        yield json.dumps(_rep(item, expand=expand))\n    yield ']}'\n\n\ndef is_expand():\n    \"\"\"Returns whether the current request is for an expanded response.\"\"\"\n\n    return flask.request.args.get('expand') is not None\n\n\ndef resource(name):\n    \"\"\"Decorates a function to handle RESTful HTTP requests for a resource.\n    \"\"\"\n    def make_responder(retriever):\n        def responder(ids):\n            entities = [retriever(id) for id in ids]\n            entities = [entity for entity in entities if entity]\n\n            if len(entities) == 1:\n                return flask.jsonify(_rep(entities[0], expand=is_expand()))\n            elif entities:\n                return app.response_class(\n                    json_generator(entities, root=name),\n                    mimetype='application/json'\n                )\n            else:\n                return flask.abort(404)\n        responder.__name__ = 'get_{0}'.format(name)\n        return responder\n    return make_responder\n\n\ndef resource_query(name):\n    \"\"\"Decorates a function to handle RESTful HTTP queries for resources.\n    \"\"\"\n    def make_responder(query_func):\n        def responder(queries):\n            return app.response_class(\n                json_generator(\n                    query_func(queries),\n                    root='results', expand=is_expand()\n                ),\n                mimetype='application/json'\n            )\n        responder.__name__ = 'query_{0}'.format(name)\n        return responder\n    return make_responder\n\n\ndef resource_list(name):\n    \"\"\"Decorates a function to handle RESTful HTTP request for a list of\n    resources.\n    \"\"\"\n    def make_responder(list_all):\n        def responder():\n            return app.response_class(\n                json_generator(list_all(), root=name, expand=is_expand()),\n                mimetype='application/json'\n            )\n        responder.__name__ = 'all_{0}'.format(name)\n        return responder\n    return make_responder\n\n\ndef _get_unique_table_field_values(model, field, sort_field):\n    \"\"\" retrieve all unique values belonging to a key from a model \"\"\"\n    if field not in model.all_keys() or sort_field not in model.all_keys():\n        raise KeyError\n    with g.lib.transaction() as tx:\n        rows = tx.query('SELECT DISTINCT \"{0}\" FROM \"{1}\" ORDER BY \"{2}\"'\n                        .format(field, model._table, sort_field))\n    return [row[0] for row in rows]\n\n\nclass IdListConverter(BaseConverter):\n    \"\"\"Converts comma separated lists of ids in urls to integer lists.\n    \"\"\"\n\n    def to_python(self, value):\n        ids = []\n        for id in value.split(','):\n            try:\n                ids.append(int(id))\n            except ValueError:\n                pass\n        return ids\n\n    def to_url(self, value):\n        return ','.join(value)\n\n\nclass QueryConverter(PathConverter):\n    \"\"\"Converts slash separated lists of queries in the url to string list.\n    \"\"\"\n\n    def to_python(self, value):\n        return value.split('/')\n\n    def to_url(self, value):\n        return ','.join(value)\n\n\n# Flask setup.\n\napp = flask.Flask(__name__)\napp.url_map.converters['idlist'] = IdListConverter\napp.url_map.converters['query'] = QueryConverter\n\n\n@app.before_request\ndef before_request():\n    g.lib = app.config['lib']\n\n\n# Items.\n\n@app.route('/item/<idlist:ids>')\n@resource('items')\ndef get_item(id):\n    return g.lib.get_item(id)\n\n\n@app.route('/item/')\n@app.route('/item/query/')\n@resource_list('items')\ndef all_items():\n    return g.lib.items()\n\n\n@app.route('/item/<int:item_id>/file')\ndef item_file(item_id):\n    item = g.lib.get_item(item_id)\n    response = flask.send_file(\n        util.py3_path(item.path),\n        as_attachment=True,\n        attachment_filename=os.path.basename(item.path),\n    )\n    response.headers['Content-Length'] = os.path.getsize(item.path)\n    return response\n\n\n@app.route('/item/query/<query:queries>')\n@resource_query('items')\ndef item_query(queries):\n    return g.lib.items(queries)\n\n\n@app.route('/item/values/<string:key>')\ndef item_unique_field_values(key):\n    sort_key = flask.request.args.get('sort_key', key)\n    try:\n        values = _get_unique_table_field_values(beets.library.Item, key,\n                                                sort_key)\n    except KeyError:\n        return flask.abort(404)\n    return flask.jsonify(values=values)\n\n\n# Albums.\n\n@app.route('/album/<idlist:ids>')\n@resource('albums')\ndef get_album(id):\n    return g.lib.get_album(id)\n\n\n@app.route('/album/')\n@app.route('/album/query/')\n@resource_list('albums')\ndef all_albums():\n    return g.lib.albums()\n\n\n@app.route('/album/query/<query:queries>')\n@resource_query('albums')\ndef album_query(queries):\n    return g.lib.albums(queries)\n\n\n@app.route('/album/<int:album_id>/art')\ndef album_art(album_id):\n    album = g.lib.get_album(album_id)\n    if album.artpath:\n        return flask.send_file(album.artpath)\n    else:\n        return flask.abort(404)\n\n\n@app.route('/album/values/<string:key>')\ndef album_unique_field_values(key):\n    sort_key = flask.request.args.get('sort_key', key)\n    try:\n        values = _get_unique_table_field_values(beets.library.Album, key,\n                                                sort_key)\n    except KeyError:\n        return flask.abort(404)\n    return flask.jsonify(values=values)\n\n\n# Artists.\n\n@app.route('/artist/')\ndef all_artists():\n    with g.lib.transaction() as tx:\n        rows = tx.query(\"SELECT DISTINCT albumartist FROM albums\")\n    all_artists = [row[0] for row in rows]\n    return flask.jsonify(artist_names=all_artists)\n\n\n# Library information.\n\n@app.route('/stats')\ndef stats():\n    with g.lib.transaction() as tx:\n        item_rows = tx.query(\"SELECT COUNT(*) FROM items\")\n        album_rows = tx.query(\"SELECT COUNT(*) FROM albums\")\n    return flask.jsonify({\n        'items': item_rows[0][0],\n        'albums': album_rows[0][0],\n    })\n\n\n# UI.\n\n@app.route('/')\ndef home():\n    return flask.render_template('index.html')\n\n\n# Plugin hook.\n\nclass WebPlugin(BeetsPlugin):\n    def __init__(self):\n        super(WebPlugin, self).__init__()\n        self.config.add({\n            'host': u'127.0.0.1',\n            'port': 8337,\n            'cors': '',\n        })\n\n    def commands(self):\n        cmd = ui.Subcommand('web', help=u'start a Web interface')\n        cmd.parser.add_option(u'-d', u'--debug', action='store_true',\n                              default=False, help=u'debug mode')\n\n        def func(lib, opts, args):\n            args = ui.decargs(args)\n            if args:\n                self.config['host'] = args.pop(0)\n            if args:\n                self.config['port'] = int(args.pop(0))\n\n            app.config['lib'] = lib\n            # Normalizes json output\n            app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False\n\n            # Enable CORS if required.\n            if self.config['cors']:\n                self._log.info(u'Enabling CORS with origin: {0}',\n                               self.config['cors'])\n                from flask.ext.cors import CORS\n                app.config['CORS_ALLOW_HEADERS'] = \"Content-Type\"\n                app.config['CORS_RESOURCES'] = {\n                    r\"/*\": {\"origins\": self.config['cors'].get(str)}\n                }\n                CORS(app)\n            # Start the web application.\n            app.run(host=self.config['host'].as_str(),\n                    port=self.config['port'].get(int),\n                    debug=opts.debug, threaded=True)\n        cmd.func = func\n        return [cmd]\n", "path": "beetsplug/web/__init__.py"}]}