diff --git a/maloja/apis/__init__.py b/maloja/apis/__init__.py index 08576ea..f226513 100644 --- a/maloja/apis/__init__.py +++ b/maloja/apis/__init__.py @@ -1,5 +1,6 @@ from . import native_v1 from .audioscrobbler import Audioscrobbler +from .audioscrobbler_legacy import AudioscrobblerLegacy from .listenbrainz import Listenbrainz import copy @@ -11,7 +12,8 @@ native_apis = [ ] standardized_apis = [ Listenbrainz(), - Audioscrobbler() + Audioscrobbler(), + AudioscrobblerLegacy() ] def init_apis(server): diff --git a/maloja/apis/audioscrobbler_legacy.py b/maloja/apis/audioscrobbler_legacy.py new file mode 100644 index 0000000..7961b17 --- /dev/null +++ b/maloja/apis/audioscrobbler_legacy.py @@ -0,0 +1,107 @@ +from ._base import APIHandler +from ._exceptions import * +from .. import database + +class AudioscrobblerLegacy(APIHandler): + __apiname__ = "Legacy Audioscrobbler" + __doclink__ = "https://web.archive.org/web/20190531021725/https://www.last.fm/api/submissions" + __aliases__ = [ + "audioscrobbler_legacy", + "audioscrobbler/1.2" + ] + + def init(self): + + # no need to save these on disk, clients can always request a new session + self.mobile_sessions = [] + self.methods = { + "handshake":self.handshake, + "nowplaying":self.now_playing, + "scrobble":self.submit_scrobble + } + self.errors = { + BadAuthException:(200,"BADAUTH"), + InvalidAuthException:(200,"BADAUTH"), + InvalidMethodException:(200,"FAILED"), + InvalidSessionKey:(200,"BADSESSION"), + ScrobblingException:(500,"FAILED") + } + + def get_method(self,pathnodes,keys): + if keys.get("hs") == 'true': return 'handshake' + else: return pathnodes[0] + + def handshake(self,pathnodes,keys): + user = keys.get("u") + auth = keys.get("a") + timestamp = keys.get("t") + apikey = keys.get("api_key") + host = keys.get("Host") + protocol = 'https' + # expect username and password + if auth is not None: + for key in database.allAPIkeys(): + if check_token(auth, key, timestamp): + sessionkey = generate_key(self.mobile_sessions) + return 200, ( + "OK\n" + f"{sessionkey}\n" + f"{protocol}://{host}/apis/audioscrobbler_legacy/nowplaying\n" + f"{protocol}://{host}/apis/audioscrobbler_legacy/scrobble\n" + ) + else: + raise InvalidAuthException() + else: + raise BadAuthException() + + def now_playing(self,pathnodes,keys): + # I see no implementation in the other compatible APIs, so I have just + # created a route that always says it was successful except if the + # session is invalid + if keys.get("s") is None or keys.get("s") not in self.mobile_sessions: + raise InvalidSessionKey() + else: + return "OK" + + def submit_scrobble(self,pathnodes,keys): + if keys.get("s") is None or keys.get("s") not in self.mobile_sessions: + raise InvalidSessionKey() + else: + for count in range(0,50): + artist_key = f"a[{count}]" + track_key = f"t[{count}]" + time_key = f"i[{count}]" + if artist_key in keys and track_key in keys: + artiststr,titlestr = keys[artist_key], keys[track_key] + try: + timestamp = int(keys[time_key]) + except: + timestamp = None + #database.createScrobble(artists,title,timestamp) + self.scrobble(artiststr,titlestr,time=timestamp) + else: + return 200,"OK" + return 200,"OK" + + +import hashlib +import random + +def md5(input): + m = hashlib.md5() + m.update(bytes(input,encoding="utf-8")) + return m.hexdigest() + +def generate_key(ls): + key = "" + for i in range(64): + key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) + ls.append(key) + return key + +def lastfm_token(password, ts): + return md5(md5(password) + ts) + +def check_token(received_token, expected_key, ts): + expected_token = lastfm_token(expected_key, ts) + return received_token == expected_token diff --git a/testing/Maloja.postman_collection.json b/testing/Maloja.postman_collection.json index 51b87c6..3ce83ea 100644 --- a/testing/Maloja.postman_collection.json +++ b/testing/Maloja.postman_collection.json @@ -308,6 +308,16 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "id": "9928c378-cf37-4e20-b653-51f5dde51192", + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { @@ -389,7 +399,7 @@ { "listen": "test", "script": { - "id": "28214541-89bf-4184-ad9b-dd49dbcfc35d", + "id": "addc7f42-1de5-4b6d-a840-bb3075bd2cdc", "exec": [ "var data = JSON.parse(responseBody);", "postman.setEnvironmentVariable(\"session_key\", data.session.key);", @@ -430,6 +440,113 @@ "response": [] } ] + }, + { + "name": "Scrobble Audioscrobbler Legacy", + "item": [ + { + "name": "Authorize", + "event": [ + { + "listen": "test", + "script": { + "id": "01f6143f-3134-4006-9792-6e61a2be323d", + "exec": [ + "var data = responseBody.split(\"\\n\");", + "postman.setEnvironmentVariable(\"session_key\", data[1]);" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "id": "b97afa75-ab8c-4099-a6cf-6b45d653a10d", + "exec": [ + "apikey = pm.variables.get(\"api_key\");", + "ts = 565566;", + "", + "token = CryptoJS.MD5(CryptoJS.MD5(apikey) + ts)", + "", + "postman.setEnvironmentVariable(\"legacy_token\", token);" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{url}}/apis/audioscrobbler_legacy/?hs=true&t=565566&a={{legacy_token}}", + "host": [ + "{{url}}" + ], + "path": [ + "apis", + "audioscrobbler_legacy", + "" + ], + "query": [ + { + "key": "hs", + "value": "true" + }, + { + "key": "t", + "value": "565566" + }, + { + "key": "a", + "value": "{{legacy_token}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Scrobble", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/apis/audioscrobbler_legacy/scrobble?t=565566&a={{legacy_token}}&s={{session_key}}", + "host": [ + "{{url}}" + ], + "path": [ + "apis", + "audioscrobbler_legacy", + "scrobble" + ], + "query": [ + { + "key": "t", + "value": "565566" + }, + { + "key": "a", + "value": "{{legacy_token}}" + }, + { + "key": "s", + "value": "{{session_key}}" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} } ], "event": [ @@ -456,28 +573,34 @@ ], "variable": [ { - "id": "0206e63b-eeb7-49cc-9824-5398b18f7736", + "id": "3e20a0c6-11fa-4976-8bcb-5c31014e40e7", "key": "url", - "value": "http://localhost:42010", - "type": "string" + "value": "http://localhost:42010" }, { - "id": "0c6402d8-dfb7-4c87-a6ca-9b6675b8d9a1", + "id": "bd31b51f-645d-4ab4-83e1-8eb407978ea8", "key": "api_key", - "value": "localdevtestkey", - "type": "string" + "value": "localdevtestkey" }, { - "id": "bae7cf4e-fe0e-490d-8446-56a8ac51373d", + "id": "5ea9cbf8-34f9-4c5e-80b3-42857f014f80", "key": "example_artist", - "value": "EXID ft. Jeremy Soule", - "type": "string" + "value": "EXID ft. Jeremy Soule" }, { - "id": "70454e83-de63-471b-a58c-8545cef4e749", + "id": "fa4d0af7-6f09-4fc6-88ee-39cb6b91b844", "key": "example_song", - "value": "Why is the Rum gone?", - "type": "string" + "value": "Why is the Rum gone?" + }, + { + "id": "e078ab40-4135-4be3-a251-9df21b2601c1", + "key": "example_artist_2", + "value": "BLACKPINK ft. Tzuyu" + }, + { + "id": "3748cc0f-2bdc-4572-8b17-94a630fa751c", + "key": "example_song_2", + "value": "POP/STARS" } ] } \ No newline at end of file