Merge branch 'lastfm-api-new' into 'develop'
[plugin, scrobbler] Use last.fm API v2 for scrobbling if API key and secret are provided See merge request funkwhale/funkwhale!1216
This commit is contained in:
commit
84981df52b
|
@ -5,6 +5,7 @@ import subprocess
|
|||
import sys
|
||||
|
||||
import persisting_theory
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -28,6 +29,19 @@ _filters = {}
|
|||
_hooks = {}
|
||||
|
||||
|
||||
class PluginCache(object):
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
|
||||
def get(self, key, default=None):
|
||||
key = ":".join([self.prefix, key])
|
||||
return cache.get(key, default)
|
||||
|
||||
def set(self, key, value, duration=None):
|
||||
key = ":".join([self.prefix, key])
|
||||
return cache.set(key, value, duration)
|
||||
|
||||
|
||||
def get_plugin_config(
|
||||
name,
|
||||
user=False,
|
||||
|
@ -38,6 +52,7 @@ def get_plugin_config(
|
|||
description=None,
|
||||
version=None,
|
||||
label=None,
|
||||
homepage=None,
|
||||
):
|
||||
conf = {
|
||||
"name": name,
|
||||
|
@ -52,6 +67,8 @@ def get_plugin_config(
|
|||
"source": source,
|
||||
"description": description,
|
||||
"version": version,
|
||||
"cache": PluginCache(name),
|
||||
"homepage": homepage,
|
||||
}
|
||||
registry[name] = conf
|
||||
return conf
|
||||
|
@ -259,6 +276,7 @@ def serialize_plugin(plugin_conf, confs):
|
|||
"values": confs.get(plugin_conf["name"], {"conf"}).get("conf"),
|
||||
"enabled": plugin_conf["name"] in confs
|
||||
and confs[plugin_conf["name"]]["enabled"],
|
||||
"homepage": plugin_conf["homepage"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
Scrobbler plugin
|
||||
================
|
||||
|
||||
A plugin that enables scrobbling to ListenBrainz and Last.fm.
|
||||
|
||||
If you're scrobbling to last.fm, you will need to create an `API account <https://www.last.fm/api/account/create>`_
|
||||
and add two variables two your .env file:
|
||||
|
||||
- ``FUNKWHALE_PLUGIN_SCROBBLER_LASTFM_API_KEY=apikey``
|
||||
- ``FUNKWHALE_PLUGIN_SCROBBLER_LASTFM_API_SECRET=apisecret``
|
|
@ -6,6 +6,7 @@ from . import scrobbler
|
|||
|
||||
# https://listenbrainz.org/lastfm-proxy
|
||||
DEFAULT_SCROBBLER_URL = "http://post.audioscrobbler.com"
|
||||
LASTFM_SCROBBLER_URL = "https://ws.audioscrobbler.com/2.0/"
|
||||
|
||||
|
||||
@plugins.register_hook(plugins.LISTENING_CREATED, PLUGIN)
|
||||
|
@ -17,23 +18,51 @@ def forward_to_scrobblers(listening, conf, **kwargs):
|
|||
password = conf.get("password")
|
||||
url = conf.get("url", DEFAULT_SCROBBLER_URL) or DEFAULT_SCROBBLER_URL
|
||||
if username and password:
|
||||
PLUGIN["logger"].info("Forwarding scrobbler to %s", url)
|
||||
session = plugins.get_session()
|
||||
session_key, now_playing_url, scrobble_url = scrobbler.handshake_v1(
|
||||
session=session, url=url, username=username, password=password
|
||||
)
|
||||
scrobbler.submit_now_playing_v1(
|
||||
session=session,
|
||||
track=listening.track,
|
||||
session_key=session_key,
|
||||
now_playing_url=now_playing_url,
|
||||
)
|
||||
scrobbler.submit_scrobble_v1(
|
||||
session=session,
|
||||
track=listening.track,
|
||||
scrobble_time=listening.creation_date,
|
||||
session_key=session_key,
|
||||
scrobble_url=scrobble_url,
|
||||
)
|
||||
if (
|
||||
PLUGIN["settings"]["lastfm_api_key"]
|
||||
and PLUGIN["settings"]["lastfm_api_secret"]
|
||||
and url == DEFAULT_SCROBBLER_URL
|
||||
):
|
||||
PLUGIN["logger"].info("Forwarding scrobble to %s", LASTFM_SCROBBLER_URL)
|
||||
session_key = PLUGIN["cache"].get(
|
||||
"lastfm:sessionkey:{}".format(listening.user.pk)
|
||||
)
|
||||
if not session_key:
|
||||
PLUGIN["logger"].debug("Authenticating…")
|
||||
session_key = scrobbler.handshake_v2(
|
||||
username=username,
|
||||
password=password,
|
||||
scrobble_url=LASTFM_SCROBBLER_URL,
|
||||
session=session,
|
||||
api_key=PLUGIN["settings"]["lastfm_api_key"],
|
||||
api_secret=PLUGIN["settings"]["lastfm_api_secret"],
|
||||
)
|
||||
PLUGIN["cache"].set(
|
||||
"lastfm:sessionkey:{}".format(listening.user.pk), session_key
|
||||
)
|
||||
scrobbler.submit_scrobble_v2(
|
||||
session=session,
|
||||
track=listening.track,
|
||||
scrobble_time=listening.creation_date,
|
||||
session_key=session_key,
|
||||
scrobble_url=LASTFM_SCROBBLER_URL,
|
||||
api_key=PLUGIN["settings"]["lastfm_api_key"],
|
||||
api_secret=PLUGIN["settings"]["lastfm_api_secret"],
|
||||
)
|
||||
|
||||
else:
|
||||
PLUGIN["logger"].info("Forwarding scrobble to %s", url)
|
||||
session_key, now_playing_url, 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,
|
||||
)
|
||||
PLUGIN["logger"].info("Scrobble sent!")
|
||||
else:
|
||||
PLUGIN["logger"].debug("No scrobbler configuration for user, skipping")
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
"""
|
||||
A plugin that enables scrobbling to ListenBrainz and Last.fm.
|
||||
|
||||
If you're scrobbling to last.fm, you will need to create an `API account <https://www.last.fm/api/account/create>`_
|
||||
and add two variables two your .env file:
|
||||
|
||||
- ``FUNKWHALE_PLUGIN_SCROBBLER_LASTFM_API_KEY=apikey``
|
||||
- ``FUNKWHALE_PLUGIN_SCROBBLER_LASTFM_API_SECRET=apisecret``
|
||||
|
||||
"""
|
||||
from config import plugins
|
||||
|
||||
PLUGIN = plugins.get_plugin_config(
|
||||
name="scrobbler",
|
||||
label="Scrobbler",
|
||||
description="A plugin that enables scrobbling to ListenBrainz and Last.fm",
|
||||
description=(
|
||||
"A plugin that enables scrobbling to ListenBrainz and Last.fm. "
|
||||
"It must be configured on the server if you use Last.fm."
|
||||
),
|
||||
homepage="https://dev.funkwhale.audio/funkwhale/funkwhale/-/blob/develop/api/funkwhale_api/contrib/scrobbler/README.rst", # noqa
|
||||
version="0.1",
|
||||
user=True,
|
||||
conf=[
|
||||
|
@ -34,8 +28,8 @@ PLUGIN = plugins.get_plugin_config(
|
|||
{"name": "username", "type": "text", "label": "Your scrobbler username"},
|
||||
{"name": "password", "type": "password", "label": "Your scrobbler password"},
|
||||
],
|
||||
# settings=[
|
||||
# {"name": "lastfm_api_key", "type": "text"},
|
||||
# {"name": "lastfm_api_secret", "type": "text"},
|
||||
# ]
|
||||
settings=[
|
||||
{"name": "lastfm_api_key", "type": "text"},
|
||||
{"name": "lastfm_api_secret", "type": "text"},
|
||||
],
|
||||
)
|
||||
|
|
|
@ -96,3 +96,66 @@ def get_scrobble_payload(track, date, suffix="[0]"):
|
|||
if date:
|
||||
data["i{}".format(suffix)] = int(date.timestamp())
|
||||
return data
|
||||
|
||||
|
||||
def get_scrobble2_payload(track, date, suffix="[0]"):
|
||||
"""
|
||||
Documentation available at https://web.archive.org/web/20190531021725/https://www.last.fm/api/submissions
|
||||
"""
|
||||
upload = track.uploads.filter(duration__gte=0).first()
|
||||
data = {
|
||||
"artist{}".format(suffix): track.artist.name,
|
||||
"track{}".format(suffix): track.title,
|
||||
"duration{}".format(suffix): upload.duration if upload else 0,
|
||||
"album{}".format(suffix): (track.album.title if track.album else "") or "",
|
||||
"trackNumber{}".format(suffix): track.position or "",
|
||||
"mbid{}".format(suffix): str(track.mbid) or "",
|
||||
"chosenByUser{}".format(suffix): "P", # Source: P = chosen by user
|
||||
}
|
||||
if date:
|
||||
offset = upload.duration / 2 if upload.duration else 0
|
||||
data["timestamp{}".format(suffix)] = int(date.timestamp()) - offset
|
||||
return data
|
||||
|
||||
|
||||
def handshake_v2(username, password, session, api_key, api_secret, scrobble_url):
|
||||
params = {
|
||||
"method": "auth.getMobileSession",
|
||||
"username": username,
|
||||
"password": password,
|
||||
"api_key": api_key,
|
||||
}
|
||||
params["api_sig"] = hash_request(params, api_secret)
|
||||
response = session.post(scrobble_url, params)
|
||||
if 'status="ok"' not in response.text:
|
||||
raise ScrobblerException(response.text)
|
||||
|
||||
session_key = response.text.split("<key>")[1].split("</key>")[0]
|
||||
return session_key
|
||||
|
||||
|
||||
def submit_scrobble_v2(
|
||||
session, track, scrobble_time, session_key, scrobble_url, api_key, api_secret,
|
||||
):
|
||||
params = {
|
||||
"method": "track.scrobble",
|
||||
"api_key": api_key,
|
||||
"sk": session_key,
|
||||
}
|
||||
params.update(get_scrobble2_payload(track, scrobble_time))
|
||||
params["api_sig"] = hash_request(params, api_secret)
|
||||
response = session.post(scrobble_url, params)
|
||||
if 'status="ok"' not in response.text:
|
||||
raise ScrobblerException(response.text)
|
||||
|
||||
|
||||
def hash_request(data, secret_key):
|
||||
string = ""
|
||||
items = data.keys()
|
||||
items = sorted(items)
|
||||
for i in items:
|
||||
string += str(i)
|
||||
string += str(data[i])
|
||||
string += secret_key
|
||||
string_to_hash = string.encode("utf8")
|
||||
return hashlib.md5(string_to_hash).hexdigest()
|
||||
|
|
|
@ -207,6 +207,7 @@ def test_serialize_plugin():
|
|||
"source": False,
|
||||
"label": "test_plugin",
|
||||
"values": None,
|
||||
"homepage": None,
|
||||
}
|
||||
|
||||
assert plugins.serialize_plugin(plugin, plugins.get_confs()) == expected
|
||||
|
@ -230,6 +231,7 @@ def test_serialize_plugin_user(factories):
|
|||
"source": False,
|
||||
"label": "test_plugin",
|
||||
"values": None,
|
||||
"homepage": None,
|
||||
}
|
||||
|
||||
assert plugins.serialize_plugin(plugin, plugins.get_confs(user)) == expected
|
||||
|
@ -242,6 +244,7 @@ def test_serialize_plugin_user_enabled(factories):
|
|||
description="Hello world",
|
||||
conf=[{"name": "foo", "type": "boolean"}],
|
||||
user=True,
|
||||
homepage="https://example.com",
|
||||
)
|
||||
|
||||
factories["common.PluginConfiguration"](
|
||||
|
@ -256,6 +259,7 @@ def test_serialize_plugin_user_enabled(factories):
|
|||
"source": False,
|
||||
"label": "test_plugin",
|
||||
"values": {"foo": "bar"},
|
||||
"homepage": "https://example.com",
|
||||
}
|
||||
|
||||
assert plugins.serialize_plugin(plugin, plugins.get_confs(user)) == expected
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[plugin, scrobbler] Use last.fm API v2 for scrobbling if API key and secret are provided
|
|
@ -493,7 +493,7 @@ export default {
|
|||
this.getSound(toPreload)
|
||||
this.nextTrackPreloaded = true
|
||||
}
|
||||
if (t > this.listenDelay || d - t < 30) {
|
||||
if (t > (d / 2)) {
|
||||
let onlyTrack = this.$store.state.queue.tracks.length === 1
|
||||
if (this.listeningRecorded != this.currentTrack) {
|
||||
this.listeningRecorded = this.currentTrack
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
<template>
|
||||
<form :class="['ui form', {loading: isLoading}]" @submit.prevent="submit">
|
||||
<form :class="['ui segment form', {loading: isLoading}]" @submit.prevent="submit">
|
||||
<h3>{{ plugin.label }}</h3>
|
||||
<div v-if="plugin.description" v-html="markdown.makeHtml(plugin.description)"></div>
|
||||
<template v-if="plugin.homepage" >
|
||||
<div class="ui small hidden divider"></div>
|
||||
<a :href="plugin.homepage" target="_blank">
|
||||
<i class="external icon"></i>
|
||||
<translate translate-context="Footer/*/List item.Link/Short, Noun">Documentation</translate>
|
||||
</a>
|
||||
</template>
|
||||
<div class="ui clearing hidden divider"></div>
|
||||
<div v-if="errors.length > 0" role="alert" class="ui negative message">
|
||||
<h4 class="header"><translate translate-context="Content/*/Error message.Title">Error while saving plugin</translate></h4>
|
||||
|
|
Loading…
Reference in New Issue