diff --git a/README.md b/README.md index 39aa026..dd589c7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Also neat: You can use your **custom artist or track images**. * [bottle.py](https://bottlepy.org/) - [GitHub](https://github.com/bottlepy/bottle) * [waitress](https://docs.pylonsproject.org/projects/waitress/) - [GitHub](https://github.com/Pylons/waitress) * [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.7.2) +* [nimrodel](https://pypi.org/project/nimrodel/) - [GitHub](https://github.com/krateng/nimrodel) (at least Version 0.4.4) * If you'd like to display images, you will need API keys for [Last.fm](https://www.last.fm/api/account/create) and [Fanart.tv](https://fanart.tv/get-an-api-key/). These are free of charge! ## How to install @@ -58,13 +59,15 @@ If you didn't install Maloja from the package (and therefore don't have it in `/ 3) Various folders have `.info` files with more information on how to use their associated features. +4) If you'd like to implement anything on top of Maloja, visit `/api_explorer`. + ## How to scrobble ### Native API If you use Plex Web or Youtube Music on Chromium, you can use the included extension (also available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/maloja-scrobbler/cfnbifdmgbnaalphodcbandoopgbfeeh)). Make sure to enter the random key Maloja generates on first startup in the extension settings. -If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key`. +If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key` - either as from-data or json. ### Standard-compliant API diff --git a/compliant_api.py b/compliant_api.py index 289af16..434de00 100644 --- a/compliant_api.py +++ b/compliant_api.py @@ -43,17 +43,12 @@ def handler(apiname,version): return cls return deco -def handle(path,keys,headers,auth): +def handle(path,keys): print("API request: " + str(path)) print("Keys:") for k in keys: - print("\t" + str(k) + ": " + str(keys.get(k))) - print("Headers:") - for h in headers: - print("\t" + str(h) + ": " + str(headers.get(h))) - print("Auth: " + str(auth)) + print("\t",k,":",keys.get(k)) - keys = {**keys,**headers} if len(path)>1 and (path[0],path[1]) in handlers: handler = handlers[(path[0],path[1])] diff --git a/database.py b/database.py index 0396ec7..5d27693 100644 --- a/database.py +++ b/database.py @@ -14,6 +14,9 @@ try: from doreah.persistence import DiskDict except: pass import doreah +# nimrodel API +from nimrodel import EAPI as API +from nimrodel import Multi # technical import os import datetime @@ -182,50 +185,10 @@ def getTrackID(artists,title): ######## -# silly patch to get old syntax working without dbserver - -# function to register all the functions to the real server -def register_subroutes(server,path): - for subpath in dbserver.handlers_get: - func = dbserver.handlers_get[subpath] - decorator = server.get(path + subpath) - decorator(func) - for subpath in dbserver.handlers_post: - func = dbserver.handlers_post[subpath] - decorator = server.post(path + subpath) - decorator(func) +dbserver = API(delay=True,path="api") -# fake server -class FakeBottle: - def __init__(self): - self.handlers_get = {} - self.handlers_post = {} - - # these functions pretend that they're the bottle decorators, but only write - # down which functions asked for them so they can later report their names - # to the real bottle server - def get(self,path): - def register(func): - self.handlers_get[path] = func - return func - return register - def post(self,path): - def register(func): - self.handlers_post[path] = func - return func - return register - - def route(self,path): - return self.get(path) - - -dbserver = FakeBottle() - - - - -@dbserver.route("/test") +@dbserver.get("test") def test_server(): apikey = request.query.get("key") response.set_header("Access-Control-Allow-Origin","*") @@ -247,9 +210,8 @@ def test_server(): ## All database functions are separated - the external wrapper only reads the request keys, converts them into lists and renames them where necessary, and puts the end result in a dict if not already so it can be returned as json -@dbserver.route("/scrobbles") -def get_scrobbles_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("scrobbles") +def get_scrobbles_external(**keys): k_filter, k_time, _, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_amount} @@ -277,9 +239,8 @@ def get_scrobbles(**keys): # return {"scrobbles":len(SCROBBLES),"tracks":len(TRACKS),"artists":len(ARTISTS)} -@dbserver.route("/numscrobbles") -def get_scrobbles_num_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("numscrobbles") +def get_scrobbles_num_external(**keys): k_filter, k_time, _, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_amount} @@ -342,9 +303,8 @@ def get_scrobbles_num(**keys): -@dbserver.route("/tracks") -def get_tracks_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("tracks") +def get_tracks_external(**keys): k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter} @@ -366,7 +326,7 @@ def get_tracks(artist=None): #ls = [t for t in tracklist if (artist in t["artists"]) or (artist==None)] -@dbserver.route("/artists") +@dbserver.get("artists") def get_artists_external(): result = get_artists() return {"list":result} @@ -380,9 +340,8 @@ def get_artists(): -@dbserver.route("/charts/artists") -def get_charts_artists_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("charts/artists") +def get_charts_artists_external(**keys): _, k_time, _, _ = uri_to_internal(keys) ckeys = {**k_time} @@ -397,9 +356,8 @@ def get_charts_artists(**keys): -@dbserver.route("/charts/tracks") -def get_charts_tracks_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("charts/tracks") +def get_charts_tracks_external(**keys): k_filter, k_time, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter, **k_time} @@ -417,9 +375,8 @@ def get_charts_tracks(**keys): -@dbserver.route("/pulse") -def get_pulse_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("pulse") +def get_pulse_external(**keys): k_filter, k_time, k_internal, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_internal, **k_amount} @@ -440,9 +397,8 @@ def get_pulse(**keys): -@dbserver.route("/performance") -def get_performance_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("performance") +def get_performance_external(**keys): k_filter, k_time, k_internal, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_internal, **k_amount} @@ -480,7 +436,7 @@ def get_performance(**keys): -@dbserver.route("/top/artists") +@dbserver.get("top/artists") def get_top_artists_external(): keys = FormsDict.decode(request.query) @@ -513,9 +469,8 @@ def get_top_artists(**keys): -@dbserver.route("/top/tracks") -def get_top_tracks_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("top/tracks") +def get_top_tracks_external(**keys): _, k_time, k_internal, _ = uri_to_internal(keys) ckeys = {**k_time, **k_internal} @@ -548,9 +503,8 @@ def get_top_tracks(**keys): -@dbserver.route("/artistinfo") -def artistInfo_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("artistinfo") +def artistInfo_external(**keys): k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter} @@ -577,9 +531,8 @@ def artistInfo(artist): -@dbserver.route("/trackinfo") -def trackInfo_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("trackinfo") +def trackInfo_external(**keys): k_filter, _, _, _ = uri_to_internal(keys,forceTrack=True) ckeys = {**k_filter} @@ -601,9 +554,8 @@ def trackInfo(artists,title): -@dbserver.get("/newscrobble") -def pseudo_post_scrobble(): - keys = FormsDict.decode(request.query) # The Dal★Shabet handler +@dbserver.get("newscrobble") +def pseudo_post_scrobble(**keys): artists = keys.get("artist") title = keys.get("title") apikey = keys.get("key") @@ -626,9 +578,8 @@ def pseudo_post_scrobble(): return {"status":"success","track":trackdict} -@dbserver.post("/newscrobble") -def post_scrobble(): - keys = FormsDict.decode(request.forms) # The Dal★Shabet handler +@dbserver.post("newscrobble") +def post_scrobble(**keys): artists = keys.get("artist") title = keys.get("title") apikey = keys.get("key") @@ -659,28 +610,26 @@ def post_scrobble(): # standard-compliant scrobbling methods -@dbserver.post("/s/") -@dbserver.get("/s/") -def sapi(path): - path = path.split("/") +@dbserver.post("s/{path}") +@dbserver.get("s/{path}") +def sapi(path:Multi,**keys): + """Scrobbles according to a standardized protocol. + + :param string path: Path according to the scrobble protocol + :param string keys: Query keys according to the scrobble protocol + """ path = list(filter(None,path)) - headers = request.headers - if request.get_header("Content-Type") is not None and "application/json" in request.get_header("Content-Type"): - keys = request.json - else: - keys = FormsDict.decode(request.params) - auth = request.auth - return compliant_api.handle(path,keys,headers,auth) + return compliant_api.handle(path,keys) -@dbserver.route("/sync") +@dbserver.get("sync") def abouttoshutdown(): sync() #sys.exit() -@dbserver.post("/newrule") +@dbserver.post("newrule") def newrule(): keys = FormsDict.decode(request.forms) apikey = keys.pop("key",None) @@ -691,7 +640,7 @@ def newrule(): db_rulestate = False -@dbserver.route("/issues") +@dbserver.get("issues") def issues_external(): #probably not even needed return issues() @@ -787,7 +736,7 @@ def issues(): return {"duplicates":duplicates,"combined":combined,"newartists":newartists,"inconsistent":inconsistent} -@dbserver.post("/importrules") +@dbserver.post("importrules") def import_rulemodule(): keys = FormsDict.decode(request.forms) apikey = keys.pop("key",None) @@ -807,7 +756,7 @@ def import_rulemodule(): -@dbserver.post("/rebuild") +@dbserver.post("rebuild") def rebuild(): keys = FormsDict.decode(request.forms) @@ -827,9 +776,8 @@ def rebuild(): -@dbserver.get("/search") -def search(): - keys = FormsDict.decode(request.query) +@dbserver.get("search") +def search(**keys): query = keys.get("query") max_ = keys.get("max") if max_ is not None: max_ = int(max_) diff --git a/maloja b/maloja index e9f5d1f..0b3bf09 100755 --- a/maloja +++ b/maloja @@ -13,7 +13,8 @@ neededmodules = [ "bottle", "waitress", "setproctitle", - "doreah" + "doreah", + "nimrodel" ] recommendedmodules = [ diff --git a/server.py b/server.py index c98fee2..5f6ef87 100755 --- a/server.py +++ b/server.py @@ -178,7 +178,8 @@ setproctitle.setproctitle("Maloja") ## start database database.start_db() -database.register_subroutes(webserver,"/api") +#database.register_subroutes(webserver,"/api") +database.dbserver.mount(server=webserver) log("Starting up Maloja server...") run(webserver, host='::', port=MAIN_PORT, server='waitress')