Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Philipp Wolfer
funkwhale
Commits
cce158b6
Commit
cce158b6
authored
Aug 26, 2020
by
Agate
💬
Browse files
[plugin, scrobbler] Use last.fm API v2 for scrobbling if API key and secret are provided
parent
00b6fb51
Changes
9
Hide whitespace changes
Inline
Side-by-side
api/config/plugins.py
View file @
cce158b6
...
...
@@ -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"
],
}
...
...
api/funkwhale_api/contrib/scrobbler/README.rst
0 → 100644
View file @
cce158b6
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``
api/funkwhale_api/contrib/scrobbler/funkwhale_ready.py
View file @
cce158b6
...
...
@@ -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"
)
api/funkwhale_api/contrib/scrobbler/funkwhale_startup.py
View file @
cce158b6
"""
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"
},
]
,
)
api/funkwhale_api/contrib/scrobbler/scrobbler.py
View file @
cce158b6
...
...
@@ -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
()
api/tests/plugins/test_plugins.py
View file @
cce158b6
...
...
@@ -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
...
...
changes/changelog.d/scrobbler.enhancement
0 → 100644
View file @
cce158b6
[plugin, scrobbler] Use last.fm API v2 for scrobbling if API key and secret are provided
\ No newline at end of file
front/src/components/audio/Player.vue
View file @
cce158b6
...
...
@@ -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
...
...
front/src/components/auth/Plugin.vue
View file @
cce158b6
<
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>
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment