diff --git a/dev/testing/Maloja.postman_collection.json b/dev/testing/Maloja.postman_collection.json index 37edaa1..7ca23a4 100644 --- a/dev/testing/Maloja.postman_collection.json +++ b/dev/testing/Maloja.postman_collection.json @@ -249,7 +249,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.artist2}}\"\n}" + "raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\"\n}" }, "url": { "raw": "{{url}}/api/newscrobble", @@ -904,4 +904,4 @@ "value": "" } ] -} \ No newline at end of file +} diff --git a/maloja/__pkginfo__.py b/maloja/__pkginfo__.py index ab4d615..7647a42 100644 --- a/maloja/__pkginfo__.py +++ b/maloja/__pkginfo__.py @@ -4,7 +4,7 @@ # you know what f*ck it # this is hardcoded for now because of that damn project / package name discrepancy # i'll fix it one day -VERSION = "3.0.2" +VERSION = "3.0.3" HOMEPAGE = "https://github.com/krateng/maloja" diff --git a/maloja/apis/native_v1.py b/maloja/apis/native_v1.py index a03bf90..099df87 100644 --- a/maloja/apis/native_v1.py +++ b/maloja/apis/native_v1.py @@ -37,6 +37,38 @@ api.__apipath__ = "mlj_1" +def add_common_args_to_docstring(filterkeys=False,limitkeys=False,delimitkeys=False,amountkeys=False): + def decorator(func): + timeformats = "Possible formats include '2022', '2022/08', '2022/08/01', '2022/W42', 'today', 'thismonth', 'monday', 'august'" + + if filterkeys: + func.__doc__ += f""" + :param string title: Track title + :param string artist: Track artist. Can be specified multiple times. + :param bool associated: Whether to include associated artists. + """ + if limitkeys: + func.__doc__ += f""" + :param string from: Start of the desired time range. Can also be called since or start. {timeformats} + :param string until: End of the desired range. Can also be called to or end. {timeformats} + :param string in: Desired range. Can also be called within or during. {timeformats} + """ + if delimitkeys: + func.__doc__ += """ + :param string step: Step, e.g. month or week. + :param int stepn: Number of base type units per step + :param int trail: How many preceding steps should be factored in. + :param bool cumulative: Instead of a fixed trail length, use all history up to this point. + """ + if amountkeys: + func.__doc__ += """ + :param int page: Page to show + :param int perpage: Entries per page. + :param int max: Legacy. Show first page with this many entries. + """ + return func + return decorator + @api.get("test") @@ -46,11 +78,13 @@ def test_server(key=None): always respond with 200. :param string key: An API key to be tested. Optional. + :return: status (String), error (String) + :rtype: Dictionary """ response.set_header("Access-Control-Allow-Origin","*") if key is not None and not apikeystore.check_key(key): response.status = 403 - return {"error":"Wrong API key"} + return {"status":"error","error":"Wrong API key"} else: response.status = 200 @@ -59,6 +93,11 @@ def test_server(key=None): @api.get("serverinfo") def server_info(): + """Returns basic information about the server. + + :return: name (String), version (Tuple), versionstring (String), db_status (String). Additional keys can be added at any point, but will not be removed within API version. + :rtype: Dictionary + """ response.set_header("Access-Control-Allow-Origin","*") @@ -76,7 +115,13 @@ def server_info(): @api.get("scrobbles") +@add_common_args_to_docstring(filterkeys=True,limitkeys=True,amountkeys=True) def get_scrobbles_external(**keys): + """Returns a list of scrobbles. + + :return: list (List) + :rtype: Dictionary + """ k_filter, k_time, _, k_amount, _ = uri_to_internal(keys,api=True) ckeys = {**k_filter, **k_time, **k_amount} @@ -89,19 +134,14 @@ def get_scrobbles_external(**keys): 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") - - return info() - - - @api.get("numscrobbles") +@add_common_args_to_docstring(filterkeys=True,limitkeys=True,amountkeys=True) def get_scrobbles_num_external(**keys): + """Returns amount of scrobbles. + + :return: amount (Integer) + :rtype: Dictionary + """ k_filter, k_time, _, k_amount, _ = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_amount} @@ -111,7 +151,13 @@ def get_scrobbles_num_external(**keys): @api.get("tracks") +@add_common_args_to_docstring(filterkeys=True) def get_tracks_external(**keys): + """Returns all tracks (optionally of an artist). + + :return: list (List) + :rtype: Dictionary + """ k_filter, _, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter} @@ -121,7 +167,12 @@ def get_tracks_external(**keys): @api.get("artists") +@add_common_args_to_docstring() def get_artists_external(): + """Returns all artists. + + :return: list (List) + :rtype: Dictionary""" result = database.get_artists() return {"list":result} @@ -130,7 +181,12 @@ def get_artists_external(): @api.get("charts/artists") +@add_common_args_to_docstring(limitkeys=True) def get_charts_artists_external(**keys): + """Returns artist charts + + :return: list (List) + :rtype: Dictionary""" _, k_time, _, _, _ = uri_to_internal(keys) ckeys = {**k_time} @@ -140,7 +196,12 @@ def get_charts_artists_external(**keys): @api.get("charts/tracks") +@add_common_args_to_docstring(filterkeys=True,limitkeys=True) def get_charts_tracks_external(**keys): + """Returns track charts + + :return: list (List) + :rtype: Dictionary""" k_filter, k_time, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter, **k_time} @@ -151,7 +212,12 @@ def get_charts_tracks_external(**keys): @api.get("pulse") +@add_common_args_to_docstring(filterkeys=True,limitkeys=True,delimitkeys=True,amountkeys=True) def get_pulse_external(**keys): + """Returns amounts of scrobbles in specified time frames + + :return: list (List) + :rtype: Dictionary""" k_filter, k_time, k_internal, k_amount, _ = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_internal, **k_amount} @@ -162,7 +228,12 @@ def get_pulse_external(**keys): @api.get("performance") +@add_common_args_to_docstring(filterkeys=True,limitkeys=True,delimitkeys=True,amountkeys=True) def get_performance_external(**keys): + """Returns artist's or track's rank in specified time frames + + :return: list (List) + :rtype: Dictionary""" k_filter, k_time, k_internal, k_amount, _ = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_internal, **k_amount} @@ -173,7 +244,12 @@ def get_performance_external(**keys): @api.get("top/artists") +@add_common_args_to_docstring(limitkeys=True,delimitkeys=True) def get_top_artists_external(**keys): + """Returns respective number 1 artists in specified time frames + + :return: list (List) + :rtype: Dictionary""" _, k_time, k_internal, _, _ = uri_to_internal(keys) ckeys = {**k_time, **k_internal} @@ -184,7 +260,12 @@ def get_top_artists_external(**keys): @api.get("top/tracks") +@add_common_args_to_docstring(limitkeys=True,delimitkeys=True) def get_top_tracks_external(**keys): + """Returns respective number 1 tracks in specified time frames + + :return: list (List) + :rtype: Dictionary""" _, k_time, k_internal, _, _ = uri_to_internal(keys) ckeys = {**k_time, **k_internal} @@ -197,7 +278,12 @@ def get_top_tracks_external(**keys): @api.get("artistinfo") +@add_common_args_to_docstring(filterkeys=True) def artist_info_external(**keys): + """Returns information about an artist + + :return: artist (String), scrobbles (Integer), position (Integer), associated (List), medals (Mapping), topweeks (Integer) + :rtype: Dictionary""" k_filter, _, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter} @@ -206,7 +292,12 @@ def artist_info_external(**keys): @api.get("trackinfo") +@add_common_args_to_docstring(filterkeys=True) def track_info_external(artist:Multi[str],**keys): + """Returns information about a track + + :return: track (Mapping), scrobbles (Integer), position (Integer), medals (Mapping), certification (String), topweeks (Integer) + :rtype: Dictionary""" # transform into a multidict so we can use our nomral uri_to_internal function keys = FormsDict(keys) for a in artist: @@ -217,35 +308,43 @@ def track_info_external(artist:Multi[str],**keys): return database.track_info(**ckeys) -@api.get("compare") -def compare_external(**keys): - return database.compare(keys["remote"]) - - @api.post("newscrobble") @authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result') -def post_scrobble(artist:Multi=None,auth_result=None,**keys): +def post_scrobble( + artist:Multi=None, + artists:list=[], + title:str="", + album:str=None, + albumartists:list=[], + duration:int=None, + length:int=None, + time:int=None, + nofix=None, + auth_result=None): """Submit a new scrobble. :param string artist: Artist. Can be submitted multiple times as query argument for multiple artists. - :param string artists: List of artists. Overwritten by artist parameter. + :param list artists: List of artists. Overwritten by artist parameter. :param string title: Title of the track. :param string album: Name of the album. Optional. - :param string albumartists: Album artists. Optional. + :param list albumartists: Album artists. Optional. :param int duration: Actual listened duration of the scrobble in seconds. Optional. :param int length: Total length of the track in seconds. Optional. :param int time: UNIX timestamp of the scrobble. Optional, not needed if scrobble is at time of request. - :param boolean nofix: Skip server-side metadata parsing. Optional. + :param flag nofix: Skip server-side metadata parsing. Optional. + + :return: status (String), track (Mapping) + :rtype: Dictionary """ rawscrobble = { - 'track_artists':artist if artist is not None else keys.get("artists"), - 'track_title':keys.get('title'), - 'album_name':keys.get('album'), - 'album_artists':keys.get('albumartists'), - 'scrobble_duration':keys.get('duration'), - 'track_length':keys.get('length'), - 'scrobble_time':int(keys.get('time')) if (keys.get('time') is not None) else None + 'track_artists':artist if artist is not None else artists, + 'track_title':title, + 'album_name':album, + 'album_artists':albumartists, + 'scrobble_duration':duration, + 'track_length':length, + 'scrobble_time':time } # for logging purposes, don't pass values that we didn't actually supply @@ -255,7 +354,7 @@ def post_scrobble(artist:Multi=None,auth_result=None,**keys): rawscrobble, client='browser' if auth_result.get('doreah_native_auth_check') else auth_result.get('client'), api='native/v1', - fix=(keys.get("nofix") is None) + fix=(nofix is None) ) if result: @@ -273,8 +372,9 @@ def post_scrobble(artist:Multi=None,auth_result=None,**keys): @api.post("importrules") -@authenticated_api +@authenticated_function(api=True) def import_rulemodule(**keys): + """Internal Use Only""" filename = keys.get("filename") remove = keys.get("remove") is not None validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" @@ -290,8 +390,9 @@ def import_rulemodule(**keys): @api.post("rebuild") -@authenticated_api +@authenticated_function(api=True) def rebuild(**keys): + """Internal Use Only""" log("Database rebuild initiated!") database.sync() dbstatus['rebuildinprogress'] = True @@ -307,6 +408,7 @@ def rebuild(**keys): @api.get("search") def search(**keys): + """Internal Use Only""" query = keys.get("query") max_ = keys.get("max") if max_ is not None: max_ = int(max_) @@ -343,8 +445,9 @@ def search(**keys): @api.post("addpicture") -@authenticated_api +@authenticated_function(api=True) def add_picture(b64,artist:Multi=[],title=None): + """Internal Use Only""" keys = FormsDict() for a in artist: keys.append("artist",a) @@ -355,8 +458,9 @@ def add_picture(b64,artist:Multi=[],title=None): @api.post("newrule") -@authenticated_api +@authenticated_function(api=True) def newrule(**keys): + """Internal Use Only""" pass # TODO after implementing new rule system #tsv.add_entry(data_dir['rules']("webmade.tsv"),[k for k in keys]) @@ -364,24 +468,28 @@ def newrule(**keys): @api.post("settings") -@authenticated_api +@authenticated_function(api=True) def set_settings(**keys): + """Internal Use Only""" malojaconfig.update(keys) @api.post("apikeys") -@authenticated_api +@authenticated_function(api=True) def set_apikeys(**keys): + """Internal Use Only""" apikeystore.update(keys) @api.post("import") -@authenticated_api +@authenticated_function(api=True) def import_scrobbles(identifier): + """Internal Use Only""" from ..thirdparty import import_scrobbles import_scrobbles(identifier) @api.get("backup") -@authenticated_api +@authenticated_function(api=True) def get_backup(**keys): + """Internal Use Only""" from ..proccontrol.tasks.backup import backup import tempfile @@ -391,8 +499,9 @@ def get_backup(**keys): return static_file(os.path.basename(archivefile),root=tmpfolder) @api.get("export") -@authenticated_api +@authenticated_function(api=True) def get_export(**keys): + """Internal Use Only""" from ..proccontrol.tasks.export import export import tempfile @@ -403,6 +512,7 @@ def get_export(**keys): @api.post("delete_scrobble") -@authenticated_api +@authenticated_function(api=True) def delete_scrobble(timestamp): + """Internal Use Only""" database.remove_scrobble(timestamp) diff --git a/maloja/pkg_global/conf.py b/maloja/pkg_global/conf.py index 6cf39fa..0588763 100644 --- a/maloja/pkg_global/conf.py +++ b/maloja/pkg_global/conf.py @@ -142,7 +142,7 @@ malojaconfig = Configuration( "dev_mode":(tp.Boolean(), "Enable developer mode", False), }, "Network":{ - "host":(tp.String(), "Host", "::", "Host for your server - most likely :: for IPv6 or 0.0.0.0 for IPv4"), + "host":(tp.String(), "Host", "*", "Host for your server, e.g. '*' for dual stack, '::' for IPv6 or '0.0.0.0' for IPv4"), "port":(tp.Integer(), "Port", 42010), }, "Technical":{ diff --git a/maloja/server.py b/maloja/server.py index 950cc69..69f126c 100644 --- a/maloja/server.py +++ b/maloja/server.py @@ -329,8 +329,9 @@ def run_server(): try: #run(webserver, host=HOST, port=MAIN_PORT, server='waitress') - log(f"Listening on {HOST}:{PORT}") - waitress.serve(webserver, host=HOST, port=PORT, threads=THREADS) + listen = f"{HOST}:{PORT}" + log(f"Listening on {listen}") + waitress.serve(webserver, listen=listen, threads=THREADS) except OSError: log("Error. Is another Maloja process already running?") raise diff --git a/pyproject.toml b/pyproject.toml index 5d516e2..b51cec9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "malojaserver" -version = "3.0.2" +version = "3.0.3" description = "Self-hosted music scrobble database" readme = "./README.md" requires-python = ">=3.6" diff --git a/requirements.txt b/requirements.txt index 60aceb1..d4f1bf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ bottle>=0.12.16 waitress>=1.3 -doreah>=1.9.0, <2 +doreah>=1.9.1, <2 nimrodel>=0.8.0 setproctitle>=1.1.10 jinja2>=2.11