From fcee13214b530a4e61e63ae50badd75c235410cc Mon Sep 17 00:00:00 2001 From: ICTman1076 Date: Thu, 1 Oct 2020 21:01:41 +0100 Subject: [PATCH 1/7] Create legacy audioscrobbler API --- maloja/apis/legacy_audioscrobber.py | 108 ++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 maloja/apis/legacy_audioscrobber.py diff --git a/maloja/apis/legacy_audioscrobber.py b/maloja/apis/legacy_audioscrobber.py new file mode 100644 index 0000000..9554acb --- /dev/null +++ b/maloja/apis/legacy_audioscrobber.py @@ -0,0 +1,108 @@ +from ._base import APIHandler +from ._exceptions import * +from .. import database + +class Audioscrobbler(APIHandler): + __apiname__ = "Legacy Audioscrobbler" + __doclink__ = "https://web.archive.org/web/20190531021725/https://www.last.fm/api/submissions" + __aliases__ = [ + "legacyaudioscrobbler" + ] + + 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,{"error":3,"message":"Invalid method"}), + InvalidSessionKey:(200,"BADSESSION"), + ScrobblingException:(500,{"error":8,"message":"Operation failed"}) + } + + def get_method(self,pathnodes,keys): + return keys.get("method") + + def handshake(self,pathnodes,keys): + user = keys.get("u") + auth = keys.get("a") + timestamp = keys.get("t") + apikey = keys.get("api_key") + # expect username and password + if user is not None and apikey is None: + receivedToken = lastfmToken(password, timestamp) + authenticated = False + for key in database.allAPIkeys(): + if checkPassword(receivedToken, key, timestamp): + authenticated = True + break + if authenticated: + sessionkey = generate_key(self.mobile_sessions) + return 200, "OK\n" + + sessionkey + "\n" + + protocol + "://"+domain+":"+port+"/apis/legacyaudioscrobbler/nowplaying" + "\n" + + protocol + "://"+domain+":"+port+"/apis/legacyaudioscrobbler/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: + iterating = True + count = 0 + while iterating: + t = "t"+str(count) # track + a = "a"+str(count) # artist + i = "i"+str(count) # timestamp + if t in keys and a in keys: + artiststr,titlestr = keys[a], keys[t] + #(artists,title) = cla.fullclean(artiststr,titlestr) + try: + timestamp = int(keys[i]) + except: + timestamp = None + #database.createScrobble(artists,title,timestamp) + self.scrobble(artiststr,titlestr,time=timestamp) + else: + 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 lastfmToken(password, ts): + return md5(md5(password), ts) + +def checkPassword(receivedToken, expectedKey, ts): + expectedToken = lastfmToken(expectedKey, ts) + return receivedToken == expectedToken \ No newline at end of file From a4722f9e55164560535576bf4580b799f598f000 Mon Sep 17 00:00:00 2001 From: ICTman1076 Date: Thu, 1 Oct 2020 21:06:38 +0100 Subject: [PATCH 2/7] Fix some bugs --- maloja/apis/legacy_audioscrobber.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maloja/apis/legacy_audioscrobber.py b/maloja/apis/legacy_audioscrobber.py index 9554acb..245c6ce 100644 --- a/maloja/apis/legacy_audioscrobber.py +++ b/maloja/apis/legacy_audioscrobber.py @@ -81,7 +81,9 @@ class Audioscrobbler(APIHandler): timestamp = None #database.createScrobble(artists,title,timestamp) self.scrobble(artiststr,titlestr,time=timestamp) + count += 1 else: + iterating = False return 200,"OK" From 38d4716dc5984ca58cad542de73de90542d6356d Mon Sep 17 00:00:00 2001 From: krateng Date: Fri, 2 Oct 2020 04:35:36 +0200 Subject: [PATCH 3/7] Extracting host from request --- maloja/apis/legacy_audioscrobber.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maloja/apis/legacy_audioscrobber.py b/maloja/apis/legacy_audioscrobber.py index 245c6ce..bdcd569 100644 --- a/maloja/apis/legacy_audioscrobber.py +++ b/maloja/apis/legacy_audioscrobber.py @@ -34,6 +34,7 @@ class Audioscrobbler(APIHandler): auth = keys.get("a") timestamp = keys.get("t") apikey = keys.get("api_key") + host = keys.get("Host") # expect username and password if user is not None and apikey is None: receivedToken = lastfmToken(password, timestamp) @@ -107,4 +108,4 @@ def lastfmToken(password, ts): def checkPassword(receivedToken, expectedKey, ts): expectedToken = lastfmToken(expectedKey, ts) - return receivedToken == expectedToken \ No newline at end of file + return receivedToken == expectedToken From 5c2928e13ba5c1fa282cbd7f8be0fb8afa0cf8da Mon Sep 17 00:00:00 2001 From: Krateng Date: Thu, 29 Oct 2020 15:56:51 +0100 Subject: [PATCH 4/7] Renaming and organization --- maloja/apis/__init__.py | 4 +++- .../{legacy_audioscrobber.py => audioscrobbler_legacy.py} | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename maloja/apis/{legacy_audioscrobber.py => audioscrobbler_legacy.py} (98%) 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/legacy_audioscrobber.py b/maloja/apis/audioscrobbler_legacy.py similarity index 98% rename from maloja/apis/legacy_audioscrobber.py rename to maloja/apis/audioscrobbler_legacy.py index bdcd569..9aede0d 100644 --- a/maloja/apis/legacy_audioscrobber.py +++ b/maloja/apis/audioscrobbler_legacy.py @@ -2,11 +2,11 @@ from ._base import APIHandler from ._exceptions import * from .. import database -class Audioscrobbler(APIHandler): +class AudioscrobblerLegacy(APIHandler): __apiname__ = "Legacy Audioscrobbler" __doclink__ = "https://web.archive.org/web/20190531021725/https://www.last.fm/api/submissions" __aliases__ = [ - "legacyaudioscrobbler" + "audioscrobbler/1.2", ] def init(self): From 8cf93c3100b154019e23efd7d05bed7b78d28873 Mon Sep 17 00:00:00 2001 From: Krateng Date: Thu, 29 Oct 2020 16:46:45 +0100 Subject: [PATCH 5/7] Implemented handshake --- maloja/apis/_base.py | 1 + maloja/apis/audioscrobbler_legacy.py | 37 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/maloja/apis/_base.py b/maloja/apis/_base.py index ab7328c..c82b4c9 100644 --- a/maloja/apis/_base.py +++ b/maloja/apis/_base.py @@ -69,6 +69,7 @@ class APIHandler: else: log("Unhandled Exception with " + self.__apiname__ + ": " + str(exceptiontype)) response.status,result = 500,{"status":"Unknown error","code":500} + raise return result #else: diff --git a/maloja/apis/audioscrobbler_legacy.py b/maloja/apis/audioscrobbler_legacy.py index 9aede0d..8bd3187 100644 --- a/maloja/apis/audioscrobbler_legacy.py +++ b/maloja/apis/audioscrobbler_legacy.py @@ -6,7 +6,7 @@ class AudioscrobblerLegacy(APIHandler): __apiname__ = "Legacy Audioscrobbler" __doclink__ = "https://web.archive.org/web/20190531021725/https://www.last.fm/api/submissions" __aliases__ = [ - "audioscrobbler/1.2", + "audioscrobbler_legacy", ] def init(self): @@ -27,28 +27,27 @@ class AudioscrobblerLegacy(APIHandler): } def get_method(self,pathnodes,keys): - return keys.get("method") + if keys.get("hs") == 'true': return 'handshake' def handshake(self,pathnodes,keys): + print(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 user is not None and apikey is None: - receivedToken = lastfmToken(password, timestamp) - authenticated = False + if auth is not None: for key in database.allAPIkeys(): - if checkPassword(receivedToken, key, timestamp): - authenticated = True - break - if authenticated: - sessionkey = generate_key(self.mobile_sessions) - return 200, "OK\n" + - sessionkey + "\n" + - protocol + "://"+domain+":"+port+"/apis/legacyaudioscrobbler/nowplaying" + "\n" + - protocol + "://"+domain+":"+port+"/apis/legacyaudioscrobbler/scrobble" + "\n" + 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: @@ -103,9 +102,9 @@ def generate_key(ls): ls.append(key) return key -def lastfmToken(password, ts): - return md5(md5(password), ts) +def lastfm_token(password, ts): + return md5(md5(password) + ts) -def checkPassword(receivedToken, expectedKey, ts): - expectedToken = lastfmToken(expectedKey, ts) - return receivedToken == expectedToken +def check_token(received_token, expected_key, ts): + expected_token = lastfm_token(expected_key, ts) + return received_token == expected_token From e8c19a05e4088e7b5b081b2f2ae94342513a4dbc Mon Sep 17 00:00:00 2001 From: Krateng Date: Thu, 29 Oct 2020 17:14:22 +0100 Subject: [PATCH 6/7] Implemented scrobbling --- maloja/apis/_base.py | 1 - maloja/apis/audioscrobbler_legacy.py | 22 +++++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/maloja/apis/_base.py b/maloja/apis/_base.py index c82b4c9..ab7328c 100644 --- a/maloja/apis/_base.py +++ b/maloja/apis/_base.py @@ -69,7 +69,6 @@ class APIHandler: else: log("Unhandled Exception with " + self.__apiname__ + ": " + str(exceptiontype)) response.status,result = 500,{"status":"Unknown error","code":500} - raise return result #else: diff --git a/maloja/apis/audioscrobbler_legacy.py b/maloja/apis/audioscrobbler_legacy.py index 8bd3187..da30357 100644 --- a/maloja/apis/audioscrobbler_legacy.py +++ b/maloja/apis/audioscrobbler_legacy.py @@ -28,9 +28,9 @@ class AudioscrobblerLegacy(APIHandler): def get_method(self,pathnodes,keys): if keys.get("hs") == 'true': return 'handshake' + else: return pathnodes[0] def handshake(self,pathnodes,keys): - print(keys) user = keys.get("u") auth = keys.get("a") timestamp = keys.get("t") @@ -66,25 +66,21 @@ class AudioscrobblerLegacy(APIHandler): if keys.get("s") is None or keys.get("s") not in self.mobile_sessions: raise InvalidSessionKey() else: - iterating = True - count = 0 - while iterating: - t = "t"+str(count) # track - a = "a"+str(count) # artist - i = "i"+str(count) # timestamp - if t in keys and a in keys: - artiststr,titlestr = keys[a], keys[t] - #(artists,title) = cla.fullclean(artiststr,titlestr) + 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[i]) + timestamp = int(keys[time_key]) except: timestamp = None #database.createScrobble(artists,title,timestamp) self.scrobble(artiststr,titlestr,time=timestamp) - count += 1 else: - iterating = False return 200,"OK" + return 200,"OK" import hashlib From 724bfd7164823514f7e6664939ec10d830ee870a Mon Sep 17 00:00:00 2001 From: Krateng Date: Thu, 29 Oct 2020 17:21:50 +0100 Subject: [PATCH 7/7] Updated postman collection --- maloja/apis/audioscrobbler_legacy.py | 5 +- testing/Maloja.postman_collection.json | 149 ++++++++++++++++++++++--- 2 files changed, 139 insertions(+), 15 deletions(-) diff --git a/maloja/apis/audioscrobbler_legacy.py b/maloja/apis/audioscrobbler_legacy.py index da30357..7961b17 100644 --- a/maloja/apis/audioscrobbler_legacy.py +++ b/maloja/apis/audioscrobbler_legacy.py @@ -7,6 +7,7 @@ class AudioscrobblerLegacy(APIHandler): __doclink__ = "https://web.archive.org/web/20190531021725/https://www.last.fm/api/submissions" __aliases__ = [ "audioscrobbler_legacy", + "audioscrobbler/1.2" ] def init(self): @@ -21,9 +22,9 @@ class AudioscrobblerLegacy(APIHandler): self.errors = { BadAuthException:(200,"BADAUTH"), InvalidAuthException:(200,"BADAUTH"), - InvalidMethodException:(200,{"error":3,"message":"Invalid method"}), + InvalidMethodException:(200,"FAILED"), InvalidSessionKey:(200,"BADSESSION"), - ScrobblingException:(500,{"error":8,"message":"Operation failed"}) + ScrobblingException:(500,"FAILED") } def get_method(self,pathnodes,keys): 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