Merge pull request #3755 from kdelwat/web-plugin-enhancements

Web plugin: support limited editing of items and albums
This commit is contained in:
Adrian Sampson 2020-09-20 21:29:52 -04:00 committed by GitHub
commit 68cdaf23bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 117 additions and 24 deletions

View file

@ -21,7 +21,7 @@ from beets import ui
from beets import util
import beets.library
import flask
from flask import g
from flask import g, jsonify
from werkzeug.routing import BaseConverter, PathConverter
import os
from unidecode import unidecode
@ -91,7 +91,20 @@ def is_expand():
return flask.request.args.get('expand') is not None
def resource(name):
def is_delete():
"""Returns whether the current delete request should remove the selected
files.
"""
return flask.request.args.get('delete') is not None
def get_method():
"""Returns the HTTP method of the current request."""
return flask.request.method
def resource(name, patchable=False):
"""Decorates a function to handle RESTful HTTP requests for a resource.
"""
def make_responder(retriever):
@ -99,34 +112,84 @@ def resource(name):
entities = [retriever(id) for id in ids]
entities = [entity for entity in entities if entity]
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
return app.response_class(
json_generator(entities, root=name),
mimetype='application/json'
)
if get_method() == "DELETE":
for entity in entities:
entity.remove(delete=is_delete())
return flask.make_response(jsonify({'deleted': True}), 200)
elif get_method() == "PATCH" and patchable:
for entity in entities:
entity.update(flask.request.get_json())
entity.try_sync(True, False) # write, don't move
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
return app.response_class(
json_generator(entities, root=name),
mimetype='application/json'
)
elif get_method() == "GET":
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
return app.response_class(
json_generator(entities, root=name),
mimetype='application/json'
)
else:
return flask.abort(404)
else:
return flask.abort(404)
return flask.abort(405)
responder.__name__ = 'get_{0}'.format(name)
return responder
return make_responder
def resource_query(name):
def resource_query(name, patchable=False):
"""Decorates a function to handle RESTful HTTP queries for resources.
"""
def make_responder(query_func):
def responder(queries):
return app.response_class(
json_generator(
query_func(queries),
root='results', expand=is_expand()
),
mimetype='application/json'
)
entities = query_func(queries)
if get_method() == "DELETE":
for entity in entities:
entity.remove(delete=is_delete())
return flask.make_response(jsonify({'deleted': True}), 200)
elif get_method() == "PATCH" and patchable:
for entity in entities:
entity.update(flask.request.get_json())
entity.try_sync(True, False) # write, don't move
return app.response_class(
json_generator(entities, root=name),
mimetype='application/json'
)
elif get_method() == "GET":
return app.response_class(
json_generator(
entities,
root='results', expand=is_expand()
),
mimetype='application/json'
)
else:
return flask.abort(405)
responder.__name__ = 'query_{0}'.format(name)
return responder
return make_responder
@ -203,8 +266,8 @@ def before_request():
# Items.
@app.route('/item/<idlist:ids>')
@resource('items')
@app.route('/item/<idlist:ids>', methods=["GET", "DELETE", "PATCH"])
@resource('items', patchable=True)
def get_item(id):
return g.lib.get_item(id)
@ -250,8 +313,8 @@ def item_file(item_id):
return response
@app.route('/item/query/<query:queries>')
@resource_query('items')
@app.route('/item/query/<query:queries>', methods=["GET", "DELETE", "PATCH"])
@resource_query('items', patchable=True)
def item_query(queries):
return g.lib.items(queries)
@ -279,7 +342,7 @@ def item_unique_field_values(key):
# Albums.
@app.route('/album/<idlist:ids>')
@app.route('/album/<idlist:ids>', methods=["GET", "DELETE"])
@resource('albums')
def get_album(id):
return g.lib.get_album(id)
@ -292,7 +355,7 @@ def all_albums():
return g.lib.albums()
@app.route('/album/query/<query:queries>')
@app.route('/album/query/<query:queries>', methods=["GET", "DELETE"])
@resource_query('albums')
def album_query(queries):
return g.lib.albums(queries)

View file

@ -152,6 +152,7 @@ New features:
all front images instead of blindly selecting the first one.
* ``beet remove`` now also allows interactive selection of items from the query
similar to ``beet modify``
* :doc:`/plugins/web`: add DELETE and PATCH methods for modifying items
Fixes:

View file

@ -183,6 +183,25 @@ representation. ::
If there is no item with that id responds with a *404* status
code.
``DELETE /item/6``
++++++++++++++++++
Removes the item with id *6* from the beets library. If the *?delete* query string is included,
the matching file will be deleted from disk.
``PATCH /item/6``
++++++++++++++++++
Updates the item with id *6* and write the changes to the music file. The body should be a JSON object
containing the changes to the object.
Returns the updated JSON representation. ::
{
"id": 6,
"title": "A Song",
...
}
``GET /item/6,12,13``
+++++++++++++++++++++
@ -192,6 +211,8 @@ the response is the same as for `GET /item/`_. It is *not guaranteed* that the
response includes all the items requested. If a track is not found it is silently
dropped from the response.
This endpoint also supports *DELETE* and *PATCH* methods as above, to operate on all
items of the list.
``GET /item/path/...``
++++++++++++++++++++++
@ -221,6 +242,8 @@ Path elements are joined as parts of a query. For example,
To specify literal path separators in a query, use a backslash instead of a
slash.
This endpoint also supports *DELETE* and *PATCH* methods as above, to operate on all
items returned by the query.
``GET /item/6/file``
++++++++++++++++++++
@ -238,10 +261,16 @@ For albums, the following endpoints are provided:
* ``GET /album/5``
* ``DELETE /album/5``
* ``GET /album/5,7``
* ``DELETE /album/5,7``
* ``GET /album/query/querystring``
* ``DELETE /album/query/querystring``
The interface and response format is similar to the item API, except replacing
the encapsulation key ``"items"`` with ``"albums"`` when requesting ``/album/``
or ``/album/5,7``. In addition we can request the cover art of an album with