From 144c11c5c600b699d1d8c752fdb11f5dfe9f25c7 Mon Sep 17 00:00:00 2001 From: Krateng Date: Sun, 12 May 2019 11:46:25 +0200 Subject: [PATCH] Experimental support for third party GNUFM scrobblers --- README.md | 18 ++++++++--- compliant_api.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ database.py | 21 +++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 compliant_api.py diff --git a/README.md b/README.md index cb21464..8dfdf3d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Also neat: You can use your **custom artist or track images**. * [python3](https://www.python.org/) - [GitHub](https://github.com/python/cpython) * [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.6.1) +* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.7.2) * 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 @@ -51,15 +51,14 @@ Also neat: You can use your **custom artist or track images**. If you didn't install Maloja from the package (and therefore don't have it in `/opt/maloja`), every command needs to be executed from the Maloja directory and led with `./`. Otherwise, all commands work in any location and without the prefix. -1) In order to scrobble your music from Plex Web or YouTube Music, install the included Chrome extension. Make sure to enter the random key Maloja generates on first startup in the extension. If you use another music player, Maloja has a very simple API to create your own scrobbler. -2) If you would like to import all your previous last.fm scrobbles, use [benfoxall's website](https://benjaminbenben.com/lastfm-to-csv/) ([GitHub page](https://github.com/benfoxall/lastfm-to-csv)). Use the command +1) If you would like to import all your previous last.fm scrobbles, use [benfoxall's website](https://benjaminbenben.com/lastfm-to-csv/) ([GitHub page](https://github.com/benfoxall/lastfm-to-csv)). Use the command maloja import *filename* to import the downloaded file into Maloja. -3) You can interact with the server at any time with the commands +2) You can interact with the server at any time with the commands maloja stop maloja restart @@ -67,3 +66,14 @@ If you didn't install Maloja from the package (and therefore don't have it in `/ maloja update The `update` command will always fetch the latest version, while packages are only offered for release versions. + + +## How to scrobble + +If you use Plex Web or Youtube Music on Chromium, you can use the included extension. Make sure to enter the random key Maloja generates on first startup in the extension settings. + +You can use any third-party scrobbler that supports the audioscrobbler protocol (GNUFM). This is still very experimental, but give it a try with these settings: + + Gnukebox URL: Your Maloja URL followed by `/api/s/audioscrobbler` + Username: Any name, doesn't matter + Password: Any of your API keys (you can define new ones in `clients/authenticated_machines` in your Maloja folder) diff --git a/compliant_api.py b/compliant_api.py new file mode 100644 index 0000000..6872fff --- /dev/null +++ b/compliant_api.py @@ -0,0 +1,79 @@ +from doreah.logging import log +import hashlib +import random +import database +from cleanup import CleanerAgent + +## GNU-FM-compliant scrobbling + + +cla = CleanerAgent() + +def md5(input): + m = hashlib.md5() + m.update(bytes(input,encoding="utf-8")) + return m.hexdigest() + + +#def check_sig(keys): +# try: +# sig = keys.pop("api_sig") +# text = "".join([key + keys[key] for key in sorted(keys.keys())]) + # secret +# assert sig == md5(text) +# return True +# except: +# return False + + + +def handle(path,keys): +# log("API REQUEST") +# log(str(path)) +# for k in keys: +# log(str(k) + ": " + str(keys.getall(k))) + + if path[0] == "audioscrobbler": + return handle_audioscrobbler(path[1:],keys) + + +# no need to save these on disk, clients can always request a new session +mobile_sessions = [] + +def handle_audioscrobbler(path,keys): + + if path[0] == "2.0": + + if keys.get("method") == "auth.getMobileSession": + token = keys.get("authToken") + user = keys.get("username") + for key in database.allAPIkeys(): + if md5(user + md5(key)) == token: + sessionkey = "" + for i in range(64): + sessionkey += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) + mobile_sessions.append(sessionkey) + return {"session":{"key":sessionkey}} + return {"error":4} + + + elif keys.get("method") == "track.scrobble": + if keys.get("sk") is None or keys.get("sk") not in mobile_sessions: + return {"error":9} + else: + + if "track" in keys and "artist" in keys: + artiststr,titlestr = keys["artist"], keys["track"] + (artists,title) = cla.fullclean(artiststr,titlestr) + timestamp = int(keys["timestamp"]) + database.createScrobble(artists,title,timestamp) + return {"scrobbles":{"@attr":{"ignored":0}}} + else: + for num in range(50): + if "track[" + str(num) + "]" in keys: + artiststr,titlestr = keys["artist[" + str(num) + "]"], keys["track[" + str(num) + "]"] + (artists,title) = cla.fullclean(artiststr,titlestr) + timestamp = int(keys["timestamp[" + str(num) + "]"]) + database.createScrobble(artists,title,timestamp) + return {"scrobbles":{"@attr":{"ignored":0}}} + + return {"error":3} diff --git a/database.py b/database.py index 811d72c..5f896a3 100644 --- a/database.py +++ b/database.py @@ -6,6 +6,7 @@ from cleanup import * from utilities import * from malojatime import * from urihandler import uri_to_internal +import compliant_api # doreah toolkit from doreah.logging import log from doreah import tsv @@ -73,6 +74,8 @@ def loadAPIkeys(): def checkAPIkey(k): return (k in [k for [k,d] in clients]) +def allAPIkeys(): + return [k for [k,d] in clients] #### @@ -610,6 +613,24 @@ def post_scrobble(): return "" + + +# standard-compliant scrobbling methods + +@dbserver.post("/s/") +def sapi(path): + path = path.split("/") + keys = FormsDict.decode(request.forms) + return compliant_api.handle(path,keys) +@dbserver.get("/s/") +def sapi(path): + path = path.split("/") + keys = FormsDict.decode(request.query) + return compliant_api.handle(path,keys) + + + + @dbserver.route("/sync") def abouttoshutdown(): sync()