mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 17:16:07 +01:00
Merge pull request #3755 from kdelwat/web-plugin-enhancements
Web plugin: support limited editing of items and albums
This commit is contained in:
commit
68cdaf23bf
3 changed files with 117 additions and 24 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue