From 44a8c114f17264e67c41332bc80ab2340ce021eb Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 24 Sep 2019 13:04:34 +0200
Subject: [PATCH] Attempt at scrobbling plugin

---
 api/plugins/fw_scrobbler/__init__.py  |  0
 api/plugins/fw_scrobbler/apps.py      | 11 ++++
 api/plugins/fw_scrobbler/config.py    | 35 ++++++++++++
 api/plugins/fw_scrobbler/hooks.py     | 33 +++++++++++
 api/plugins/fw_scrobbler/scrobbler.py | 82 +++++++++++++++++++++++++++
 5 files changed, 161 insertions(+)
 create mode 100644 api/plugins/fw_scrobbler/__init__.py
 create mode 100644 api/plugins/fw_scrobbler/apps.py
 create mode 100644 api/plugins/fw_scrobbler/config.py
 create mode 100644 api/plugins/fw_scrobbler/hooks.py
 create mode 100644 api/plugins/fw_scrobbler/scrobbler.py

diff --git a/api/plugins/fw_scrobbler/__init__.py b/api/plugins/fw_scrobbler/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/plugins/fw_scrobbler/apps.py b/api/plugins/fw_scrobbler/apps.py
new file mode 100644
index 0000000000..7cc5d26838
--- /dev/null
+++ b/api/plugins/fw_scrobbler/apps.py
@@ -0,0 +1,11 @@
+from funkwhale_api import plugins
+
+
+class Plugin(plugins.Plugin):
+    name = "fw_scrobbler"
+    help = "A simple plugin that enables scrobbling to ListenBrainz and Last.fm"
+    version = "0.1"
+
+    def load(self):
+        from . import config
+        from . import hooks
diff --git a/api/plugins/fw_scrobbler/config.py b/api/plugins/fw_scrobbler/config.py
new file mode 100644
index 0000000000..186ad5e563
--- /dev/null
+++ b/api/plugins/fw_scrobbler/config.py
@@ -0,0 +1,35 @@
+from funkwhale_api import plugins
+
+plugin = plugins.get_plugin("fw_scrobbler")
+service = plugins.config.SettingSection("service", "Scrobbling Service")
+
+
+@plugin.user_settings.register
+class URL(plugins.config.StringSetting):
+    section = service
+    name = "url"
+    default = ""
+    verbose_name = "URL of the scrobbler service"
+    help = (
+        "Suggested choices:\n\n",
+        "- LastFM (default if left empty): http://post.audioscrobbler.com\n",
+        "- ListenBrainz: http://proxy.listenbrainz.org/",
+        "- ListenBrainz: http://proxy.listenbrainz.org/",
+        "- Libre.fm: http://turtle.libre.fm/",
+    )
+
+
+@plugin.user_settings.register
+class Username(plugins.config.StringSetting):
+    section = service
+    name = "username"
+    default = ""
+    verbose_name = "Your scrobbler username"
+
+
+@plugin.user_settings.register
+class Password(plugins.config.PasswordSetting):
+    section = service
+    name = "password"
+    default = ""
+    verbose_name = "Your scrobbler password"
diff --git a/api/plugins/fw_scrobbler/hooks.py b/api/plugins/fw_scrobbler/hooks.py
new file mode 100644
index 0000000000..4769bbf723
--- /dev/null
+++ b/api/plugins/fw_scrobbler/hooks.py
@@ -0,0 +1,33 @@
+from funkwhale_api import plugins
+
+from . import scrobbler
+
+plugin = plugins.get_plugin("fw_scrobbler")
+
+# https://listenbrainz.org/lastfm-proxy
+DEFAULT_SCROBBLER_URL = "http://post.audioscrobbler.com"
+
+
+@plugin.hooks.connect("history.listening.created")
+def forward_to_scrobblers(listening, plugin_conf, **kwargs):
+    if plugin_conf["user"] is None:
+        raise plugins.Skip()
+
+    username = plugin_conf["user"]["settings"].get("service__username")
+    password = plugin_conf["user"]["settings"].get("service__password")
+    url = plugin_conf["user"]["settings"].get("service__url", DEFAULT_SCROBBLER_URL)
+    if username and password:
+        plugin.logger.info("Forwarding scrobbler to %s", url)
+        session = plugin.get_requests_session()
+        session_key, scrobble_url = scrobbler.handshake_v1(
+            session=session, url=url, username=username, password=password
+        )
+        scrobbler.submit_scrobble_v1(
+            session=session,
+            track=listening.track,
+            scrobble_time=listening.creation_date,
+            session_key=session_key,
+            scrobble_url=scrobble_url,
+        )
+    else:
+        plugin.logger.debug("No scrobbler configuration for user, skipping")
diff --git a/api/plugins/fw_scrobbler/scrobbler.py b/api/plugins/fw_scrobbler/scrobbler.py
new file mode 100644
index 0000000000..84653ca6cc
--- /dev/null
+++ b/api/plugins/fw_scrobbler/scrobbler.py
@@ -0,0 +1,82 @@
+import hashlib
+import time
+
+import time
+
+from funkwhale_api import plugins
+
+from . import scrobbler
+
+# https://github.com/jlieth/legacy-scrobbler
+plugin = plugins.get_plugin("fw_scrobbler")
+
+
+class ScrobblerException(Exception):
+    pass
+
+
+def handshake_v1(session, url, username, password):
+    timestamp = str(int(time.time())).encode("utf-8")
+    password_hash = hashlib.md5(password.encode("utf-8")).hexdigest()
+    auth = hashlib.md5(password_hash.encode("utf-8") + timestamp).hexdigest()
+    params = {
+        "hs": "true",
+        "p": "1.2",
+        "c": plugin.name,
+        "v": plugin.version,
+        "u": username,
+        "t": timestamp,
+        "a": auth,
+    }
+
+    session = plugin.get_requests_session()
+    plugin.logger.debug(
+        "Performing scrobbler handshake for username %s at %s", username, url
+    )
+    handshake_response = session.get(url, params=params)
+    # process response
+    result = handshake_response.text.split("\n")
+    if len(result) >= 4 and result[0] == "OK":
+        session_key = result[1]
+        # nowplaying_url = result[2]
+        scrobble_url = result[3]
+    elif result[0] == "BANNED":
+        raise ScrobblerException("BANNED")
+    elif result[0] == "BADAUTH":
+        raise ScrobblerException("BADAUTH")
+    elif result[0] == "BADTIME":
+        raise ScrobblerException("BADTIME")
+    else:
+        raise ScrobblerException(handshake_response.text)
+
+    plugin.logger.debug("Handshake successful, scrobble url: %s", scrobble_url)
+    return session_key, scrobble_url
+
+
+def submit_scrobble_v1(session, scrobble_time, track, session_key, scrobble_url):
+    payload = get_scrobble_payload(track, scrobble_time)
+    plugin.logger.debug("Sending scrobble with payload %s", payload)
+    payload["s"] = session_key
+    response = session.post(scrobble_url, payload)
+    response.raise_for_status()
+    if response.text.startswith("OK"):
+        return
+    elif response.text.startswith("BADSESSION"):
+        raise ScrobblerException("Remote server says the session is invalid")
+    else:
+        raise ScrobblerException(response.text)
+
+    plugin.logger.debug("Scrobble successfull!")
+
+
+def get_scrobble_payload(track, scrobble_time):
+    upload = track.uploads.filter(duration__gte=0).first()
+    return {
+        "a[0]": track.artist.name,
+        "t[0]": track.title,
+        "i[0]": int(scrobble_time.timestamp()),
+        "l[0]": upload.duration if upload else 0,
+        "b[0]": track.album.title or "",
+        "n[0]": track.position or "",
+        "m[0]": str(track.mbid) or "",
+    }
-- 
GitLab