diff --git a/maloja/apis/__init__.py b/maloja/apis/__init__.py new file mode 100644 index 0000000..bf20dfa --- /dev/null +++ b/maloja/apis/__init__.py @@ -0,0 +1,17 @@ +from . import native_v1 +import copy + + +apis = { + "mlj_1":native_v1.api +} + + +def init_apis(server): + + for api in apis: + apis[api].mount(server=server,path="apis/"+api) + + # backwards compatibility + nativeapi = copy.deepcopy(apis["mlj_1"]) + nativeapi.mount(server=server,path="api") diff --git a/maloja/apis/native_v1.py b/maloja/apis/native_v1.py new file mode 100644 index 0000000..9ad9b1f --- /dev/null +++ b/maloja/apis/native_v1.py @@ -0,0 +1,314 @@ +from ..database import * +from doreah import settings +from ..__pkginfo__ import version +from ..malojauri import uri_to_internal +from .. import utilities + +from bottle import response + +# nimrodel API +from nimrodel import EAPI as API +from nimrodel import Multi + + +api = API(delay=True) + + +@api.get("test") +def test_server(key=None): + response.set_header("Access-Control-Allow-Origin","*") + if key is not None and not (checkAPIkey(key)): + response.status = 403 + return {"error":"Wrong API key"} + + else: + response.status = 200 + return {"status":"ok"} + + # 200 Database server is up and operational + # 403 Database server is up, but provided API key is not valid + +@api.get("serverinfo") +def server_info(): + + + response.set_header("Access-Control-Allow-Origin","*") + response.set_header("Content-Type","application/json") + + return { + "name":settings.get_settings("NAME"), + "version":version, + "versionstring":".".join(str(n) for n in version) + } + + +## API ENDPOINTS THAT CLOSELY MATCH ONE DATABASE FUNCTION + + +@api.get("scrobbles") +def get_scrobbles_external(**keys): + k_filter, k_time, _, k_amount = uri_to_internal(keys) + ckeys = {**k_filter, **k_time, **k_amount} + + result = get_scrobbles(**ckeys) + return {"list":result} + + +# info for comparison +@api.get("info") +def info_external(**keys): + + response.set_header("Access-Control-Allow-Origin","*") + response.set_header("Content-Type","application/json") + + result = info() + return result + + + +@api.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} + + result = get_scrobbles_num(**ckeys) + return {"amount":result} + + + +@api.get("tracks") +def get_tracks_external(**keys): + k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) + ckeys = {**k_filter} + + result = get_tracks(**ckeys) + return {"list":result} + + + +@api.get("artists") +def get_artists_external(): + result = get_artists() + return {"list":result} + + + + + +@api.get("charts/artists") +def get_charts_artists_external(**keys): + _, k_time, _, _ = uri_to_internal(keys) + ckeys = {**k_time} + + result = get_charts_artists(**ckeys) + return {"list":result} + + + +@api.get("charts/tracks") +def get_charts_tracks_external(**keys): + k_filter, k_time, _, _ = uri_to_internal(keys,forceArtist=True) + ckeys = {**k_filter, **k_time} + + result = get_charts_tracks(**ckeys) + return {"list":result} + + + + +@api.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} + + results = get_pulse(**ckeys) + return {"list":results} + + + + +@api.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} + + results = get_performance(**ckeys) + return {"list":results} + + + + +@api.get("top/artists") +def get_top_artists_external(**keys): + _, k_time, k_internal, _ = uri_to_internal(keys) + ckeys = {**k_time, **k_internal} + + results = get_top_artists(**ckeys) + return {"list":results} + + + + +@api.get("top/tracks") +def get_top_tracks_external(**keys): + _, k_time, k_internal, _ = uri_to_internal(keys) + ckeys = {**k_time, **k_internal} + + # IMPLEMENT THIS FOR TOP TRACKS OF ARTIST AS WELL? + + results = get_top_tracks(**ckeys) + return {"list":results} + + + + +@api.get("artistinfo") +def artistInfo_external(**keys): + k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) + ckeys = {**k_filter} + + results = artistInfo(**ckeys) + return results + + + +@api.get("trackinfo") +def trackInfo_external(artist:Multi[str],**keys): + # transform into a multidict so we can use our nomral uri_to_internal function + keys = FormsDict(keys) + for a in artist: + keys.append("artist",a) + k_filter, _, _, _ = uri_to_internal(keys,forceTrack=True) + ckeys = {**k_filter} + + results = trackInfo(**ckeys) + return results + + +@api.get("compare") +def compare_external(**keys): + + results = compare(keys["remote"]) + return results + + + +@api.get("newscrobble") +@api.post("newscrobble") +@authenticated_api_with_alternate(api_key_correct) +def post_scrobble(artist:Multi,**keys): + artists = "/".join(artist) + title = keys.get("title") + album = keys.get("album") + duration = keys.get("seconds") + time = keys.get("time") + if time is not None: time = int(time) + + return incoming_scrobble(artists,title,album=album,duration=duration,time=time) + + + + +@api.post("importrules") +@authenticated_api +def import_rulemodule(**keys): + filename = keys.get("filename") + remove = keys.get("remove") is not None + validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + filename = "".join(c for c in filename if c in validchars) + + if remove: + log("Deactivating predefined rulefile " + filename) + os.remove(datadir("rules/" + filename + ".tsv")) + else: + log("Importing predefined rulefile " + filename) + os.symlink(datadir("rules/predefined/" + filename + ".tsv"),datadir("rules/" + filename + ".tsv")) + + + +@api.post("rebuild") +@authenticated_api +def rebuild(**keys): + log("Database rebuild initiated!") + sync() + from .proccontrol.tasks.fixexisting import fix + fix() + global cla, coa + cla = CleanerAgent() + coa = CollectorAgent() + build_db() + invalidate_caches() + + + + +@api.get("search") +def search(**keys): + query = keys.get("query") + max_ = keys.get("max") + if max_ is not None: max_ = int(max_) + query = query.lower() + + artists = db_search(query,type="ARTIST") + tracks = db_search(query,type="TRACK") + + + + # if the string begins with the query it's a better match, if a word in it begins with it, still good + # also, shorter is better (because longer titles would be easier to further specify) + artists.sort(key=lambda x: ((0 if x.lower().startswith(query) else 1 if " " + query in x.lower() else 2),len(x))) + tracks.sort(key=lambda x: ((0 if x["title"].lower().startswith(query) else 1 if " " + query in x["title"].lower() else 2),len(x["title"]))) + + # add links + artists_result = [] + for a in artists: + result = {"name":a} + result["link"] = "/artist?" + compose_querystring(internal_to_uri({"artist":a})) + result["image"] = "/image?" + compose_querystring(internal_to_uri({"artist":a})) + artists_result.append(result) + + tracks_result = [] + for t in tracks: + result = t + result["link"] = "/track?" + compose_querystring(internal_to_uri({"track":t})) + result["image"] = "/image?" + compose_querystring(internal_to_uri({"track":t})) + tracks_result.append(result) + + return {"artists":artists_result[:max_],"tracks":tracks_result[:max_]} + + +@api.post("addpicture") +@authenticated_api +def add_picture(b64,artist:Multi=[],title=None): + keys = FormsDict() + for a in artist: + keys.append("artist",a) + if title is not None: keys.append("title",title) + k_filter, _, _, _ = uri_to_internal(keys) + if "track" in k_filter: k_filter = k_filter["track"] + utilities.set_image(b64,**k_filter) + + + + + +# standard-compliant scrobbling methods + +@api.post("s/{path}",pass_headers=True) +@api.get("s/{path}",pass_headers=True) +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)) + return compliant_api.handle(path,keys) + + + +@api.post("newrule") +@authenticated_api +def newrule(**keys): + tsv.add_entry(datadir("rules/webmade.tsv"),[k for k in keys]) + #addEntry("rules/webmade.tsv",[k for k in keys]) diff --git a/maloja/database.py b/maloja/database.py index f3daa39..0ddd751 100644 --- a/maloja/database.py +++ b/maloja/database.py @@ -24,9 +24,7 @@ try: except: pass import doreah -# nimrodel API -from nimrodel import EAPI as API -from nimrodel import Multi + # technical import os @@ -260,45 +258,9 @@ def api_key_correct(request): return checkAPIkey(apikey) -dbserver = API(delay=True,path="api") -@dbserver.get("test") -def test_server(key=None): - response.set_header("Access-Control-Allow-Origin","*") - if key is not None and not (checkAPIkey(key)): - response.status = 403 - return "Wrong API key" - else: - response.status = 200 - return - - # 200 Database server is up and operational - # 403 Database server is up, but provided API key is not valid - -@dbserver.get("serverinfo") -def server_info(): - - - response.set_header("Access-Control-Allow-Origin","*") - response.set_header("Content-Type","application/json") - - return { - "name":settings.get_settings("NAME"), - "version":version, - "versionstring":".".join(str(n) for n in version) - } - -## 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.get("scrobbles") -def get_scrobbles_external(**keys): - k_filter, k_time, _, k_amount = uri_to_internal(keys) - ckeys = {**k_filter, **k_time, **k_amount} - - result = get_scrobbles(**ckeys) - return {"list":result} def get_scrobbles(**keys): r = db_query(**{k:keys[k] for k in keys if k in ["artist","artists","title","since","to","within","timerange","associated","track","max_"]}) @@ -308,15 +270,6 @@ def get_scrobbles(**keys): # return r return r -# info for comparison -@dbserver.get("info") -def info_external(**keys): - - response.set_header("Access-Control-Allow-Origin","*") - response.set_header("Content-Type","application/json") - - result = info() - return result def info(): totalscrobbles = get_scrobbles_num() @@ -333,23 +286,6 @@ def info(): -# UNUSED -#@dbserver.route("/amounts") -#def get_amounts_external(): -# return get_amounts() #really now -# -#def get_amounts(): -# return {"scrobbles":len(SCROBBLES),"tracks":len(TRACKS),"artists":len(ARTISTS)} - - -@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} - - result = get_scrobbles_num(**ckeys) - return {"amount":result} - def get_scrobbles_num(**keys): r = db_query(**{k:keys[k] for k in keys if k in ["artist","track","artists","title","since","to","within","timerange","associated"]}) return len(r) @@ -387,32 +323,6 @@ def get_scrobbles_num(**keys): # return validtracks -# UNUSED -#@dbserver.route("/charts") -#def get_charts_external(): -# keys = FormsDict.decode(request.query) -# ckeys = {} -# ckeys["since"], ckeys["to"], ckeys["within"] = keys.get("since"), keys.get("to"), keys.get("in") -# -# result = get_scrobbles_num(**ckeys) -# return {"number":result} - -#def get_charts(**keys): -# return db_aggregate(**{k:keys[k] for k in keys if k in ["since","to","within"]}) - - - - - - - -@dbserver.get("tracks") -def get_tracks_external(**keys): - k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) - ckeys = {**k_filter} - - result = get_tracks(**ckeys) - return {"list":result} def get_tracks(artist=None): @@ -429,11 +339,6 @@ def get_tracks(artist=None): #ls = [t for t in tracklist if (artist in t["artists"]) or (artist==None)] -@dbserver.get("artists") -def get_artists_external(): - result = get_artists() - return {"list":result} - def get_artists(): return ARTISTS #well @@ -442,15 +347,6 @@ def get_artists(): - -@dbserver.get("charts/artists") -def get_charts_artists_external(**keys): - _, k_time, _, _ = uri_to_internal(keys) - ckeys = {**k_time} - - result = get_charts_artists(**ckeys) - return {"list":result} - def get_charts_artists(**keys): return db_aggregate(by="ARTIST",**{k:keys[k] for k in keys if k in ["since","to","within","timerange"]}) @@ -459,13 +355,6 @@ def get_charts_artists(**keys): -@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} - - result = get_charts_tracks(**ckeys) - return {"list":result} def get_charts_tracks(**keys): return db_aggregate(by="TRACK",**{k:keys[k] for k in keys if k in ["since","to","within","timerange","artist"]}) @@ -478,14 +367,6 @@ def get_charts_tracks(**keys): -@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} - - results = get_pulse(**ckeys) - return {"list":results} - def get_pulse(**keys): rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]}) @@ -500,14 +381,6 @@ def get_pulse(**keys): -@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} - - results = get_performance(**ckeys) - return {"list":results} - def get_performance(**keys): rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]}) @@ -539,14 +412,6 @@ def get_performance(**keys): -@dbserver.get("top/artists") -def get_top_artists_external(**keys): - _, k_time, k_internal, _ = uri_to_internal(keys) - ckeys = {**k_time, **k_internal} - - results = get_top_artists(**ckeys) - return {"list":results} - def get_top_artists(**keys): rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]}) @@ -569,17 +434,6 @@ def get_top_artists(**keys): - -@dbserver.get("top/tracks") -def get_top_tracks_external(**keys): - _, k_time, k_internal, _ = uri_to_internal(keys) - ckeys = {**k_time, **k_internal} - - # IMPLEMENT THIS FOR TOP TRACKS OF ARTIST AS WELL? - - results = get_top_tracks(**ckeys) - return {"list":results} - def get_top_tracks(**keys): rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]}) @@ -604,14 +458,6 @@ def get_top_tracks(**keys): -@dbserver.get("artistinfo") -def artistInfo_external(**keys): - k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) - ckeys = {**k_filter} - - results = artistInfo(**ckeys) - return results - def artistInfo(artist): charts = db_aggregate(by="ARTIST") @@ -642,17 +488,6 @@ def artistInfo(artist): -@dbserver.get("trackinfo") -def trackInfo_external(artist:Multi[str],**keys): - # transform into a multidict so we can use our nomral uri_to_internal function - keys = FormsDict(keys) - for a in artist: - keys.append("artist",a) - k_filter, _, _, _ = uri_to_internal(keys,forceTrack=True) - ckeys = {**k_filter} - - results = trackInfo(**ckeys) - return results def trackInfo(track): charts = db_aggregate(by="TRACK") @@ -679,11 +514,6 @@ def trackInfo(track): -@dbserver.get("compare") -def compare_external(**keys): - - results = compare(keys["remote"]) - return results def compare(remoteurl): import json @@ -737,58 +567,25 @@ def compare(remoteurl): } - -@dbserver.get("newscrobble") -@dbserver.post("newscrobble") -@authenticated_api_with_alternate(api_key_correct) -def post_scrobble(artist:Multi,**keys): - artists = "/".join(artist) - title = keys.get("title") - album = keys.get("album") - duration = keys.get("seconds") - - try: - time = int(keys.get("time")) - except: +def incoming_scrobble(artists,title,album=None,duration=None,time=None): + artists = "/".join(artists) + if time is None: time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) - log("Incoming scrobble (native API): ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug") + log("Incoming scrobble (): ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug") (artists,title) = cla.fullclean(artists,title) - - ## this is necessary for localhost testing - #response.set_header("Access-Control-Allow-Origin","*") - trackdict = createScrobble(artists,title,time,album,duration) sync() - #always sync, one filesystem access every three minutes shouldn't matter - - return {"status":"success","track":trackdict} -# standard-compliant scrobbling methods - -@dbserver.post("s/{path}",pass_headers=True) -@dbserver.get("s/{path}",pass_headers=True) -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)) - return compliant_api.handle(path,keys) -@dbserver.post("newrule") -@authenticated_api -def newrule(**keys): - tsv.add_entry(datadir("rules/webmade.tsv"),[k for k in keys]) - #addEntry("rules/webmade.tsv",[k for k in keys]) + def issues(): @@ -931,84 +728,6 @@ def get_predefined_rulesets(): return rulesets -@dbserver.post("importrules") -@authenticated_api -def import_rulemodule(**keys): - filename = keys.get("filename") - remove = keys.get("remove") is not None - validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - filename = "".join(c for c in filename if c in validchars) - - if remove: - log("Deactivating predefined rulefile " + filename) - os.remove(datadir("rules/" + filename + ".tsv")) - else: - log("Importing predefined rulefile " + filename) - os.symlink(datadir("rules/predefined/" + filename + ".tsv"),datadir("rules/" + filename + ".tsv")) - - - -@dbserver.post("rebuild") -@authenticated_api -def rebuild(**keys): - log("Database rebuild initiated!") - sync() - from .proccontrol.tasks.fixexisting import fix - fix() - global cla, coa - cla = CleanerAgent() - coa = CollectorAgent() - build_db() - invalidate_caches() - - - - -@dbserver.get("search") -def search(**keys): - query = keys.get("query") - max_ = keys.get("max") - if max_ is not None: max_ = int(max_) - query = query.lower() - - artists = db_search(query,type="ARTIST") - tracks = db_search(query,type="TRACK") - - - - # if the string begins with the query it's a better match, if a word in it begins with it, still good - # also, shorter is better (because longer titles would be easier to further specify) - artists.sort(key=lambda x: ((0 if x.lower().startswith(query) else 1 if " " + query in x.lower() else 2),len(x))) - tracks.sort(key=lambda x: ((0 if x["title"].lower().startswith(query) else 1 if " " + query in x["title"].lower() else 2),len(x["title"]))) - - # add links - artists_result = [] - for a in artists: - result = {"name":a} - result["link"] = "/artist?" + compose_querystring(internal_to_uri({"artist":a})) - result["image"] = "/image?" + compose_querystring(internal_to_uri({"artist":a})) - artists_result.append(result) - - tracks_result = [] - for t in tracks: - result = t - result["link"] = "/track?" + compose_querystring(internal_to_uri({"track":t})) - result["image"] = "/image?" + compose_querystring(internal_to_uri({"track":t})) - tracks_result.append(result) - - return {"artists":artists_result[:max_],"tracks":tracks_result[:max_]} - - -@dbserver.post("addpicture") -@authenticated_api -def add_picture(b64,artist:Multi=[],title=None): - keys = FormsDict() - for a in artist: - keys.append("artist",a) - if title is not None: keys.append("title",title) - k_filter, _, _, _ = uri_to_internal(keys) - if "track" in k_filter: k_filter = k_filter["track"] - utilities.set_image(b64,**k_filter) #### ## Server operation diff --git a/maloja/server.py b/maloja/server.py index 7ead1d1..603acbf 100755 --- a/maloja/server.py +++ b/maloja/server.py @@ -55,6 +55,9 @@ DATAFOLDER = DATA_DIR webserver = Bottle() auth.authapi.mount(server=webserver) +from .apis import init_apis +init_apis(webserver) + pthjoin = os.path.join def generate_css(): @@ -246,7 +249,6 @@ setproctitle.setproctitle("Maloja") ## start database database.start_db() -database.dbserver.mount(server=webserver) log("Starting up Maloja server...") #run(webserver, host=HOST, port=MAIN_PORT, server='waitress')