1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 08:56:17 +00:00

Initial ListenBrainz support

This commit is contained in:
Iván Ávalos 2024-03-18 03:30:56 -06:00
parent 1feaae7637
commit a39f2fb16a
No known key found for this signature in database
GPG Key ID: AC47C0E4F348CE33
12 changed files with 197 additions and 1 deletions

View File

@ -13,6 +13,7 @@ from ..db import Track, Album, Artist, Folder
from ..db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder
from ..db import RatingTrack, RatingFolder
from ..lastfm import LastFm
from ..listenbrainz import ListenBrainz
from . import get_entity, get_entity_id, api_routing
from .exceptions import AggregateException, GenericError, MissingParameter, NotFound
@ -175,10 +176,13 @@ def scrobble():
t = int(t) / 1000 if t else int(time.time())
lfm = LastFm(current_app.config["LASTFM"], request.user)
lbz = ListenBrainz(current_app.config["LISTENBRAINZ"], request.user)
if submission in (None, "", True, "true", "True", 1, "1"):
lfm.scrobble(res, t)
lbz.scrobble(res, t)
else:
lfm.now_playing(res)
lbz.now_playing(res)
return request.formatter.empty

View File

@ -52,6 +52,7 @@ class DefaultConfig:
"log_rotate": True,
}
LASTFM = {"api_key": None, "secret": None}
LISTENBRAINZ = {"api_url": "https://api.listenbrainz.org"}
TRANSCODING = {}
MIMETYPES = {}

View File

@ -31,7 +31,7 @@ from playhouse.db_url import parseresult_to_dict, schemes
from urllib.parse import urlparse
from uuid import UUID, uuid4
SCHEMA_VERSION = "20230331"
SCHEMA_VERSION = "20240318"
def now():
@ -439,6 +439,11 @@ class User(_Model):
default=True
) # True: ok/unlinked, False: invalid session
listenbrainz_session = FixedCharField(36, null=True)
listenbrainz_status = BooleanField(
default=True
) # True: ok/unlinked, False: invalid token
last_play = ForeignKeyField(Track, null=True, backref="+")
last_play_date = DateTimeField(null=True)

View File

@ -13,6 +13,7 @@ from functools import wraps
from ..db import ClientPrefs, User
from ..lastfm import LastFm
from ..listenbrainz import ListenBrainz
from ..managers.user import UserManager
from . import admin_only, frontend
@ -297,6 +298,30 @@ def lastfm_unreg(uid, user):
return redirect(url_for("frontend.user_profile", uid=uid))
@frontend.route("/user/<uid>/listenbrainz/link")
@me_or_uuid
def listenbrainz_reg(uid, user):
token = request.args.get("token")
if not token:
flash("Missing ListenBrainz auth token")
return redirect(url_for("frontend.user_profile", uid=uid))
lbz = ListenBrainz(current_app.config["LISTENBRAINZ"], user)
status, error = lbz.link_account(token)
flash(error if not status else "Successfully linked ListenBrainz account")
return redirect(url_for("frontend.user_profile", uid=uid))
@frontend.route("/user/<uid>/listenbrainz/unlink")
@me_or_uuid
def listenbrainz_unreg(uid, user):
lbz = ListenBrainz(current_app.config["LISTENBRAINZ"], user)
lbz.unlink_account()
flash("Unlinked ListenBrainz account")
return redirect(url_for("frontend.user_profile", uid=uid))
@frontend.route("/user/login", methods=["GET", "POST"])
def login():
return_url = request.args.get("returnUrl") or url_for("frontend.index")

127
supysonic/listenbrainz.py Normal file
View File

@ -0,0 +1,127 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
# Copyright (C) 2013-2022 Alban 'spl0k' Féron
# Copyright (C) 2024 Iván Ávalos
#
# Distributed under terms of the GNU AGPLv3 license.
import hashlib
import logging
import requests
import json
from urllib.parse import urljoin
logger = logging.getLogger(__name__)
class ListenBrainz:
def __init__(self, config, user):
if config["api_url"] is not None:
self.__api_url = config["api_url"]
self.__enabled = True
else:
self.__enabled = False
self.__user = user
def link_account(self, token):
if not self.__enabled:
return False, "No ListenBrainz URL set"
res = self.__api_request(False, "/1/validate-token", token)
if not res:
return False, "Error connecting to ListenBrainz"
else:
if "valid" in res and res["valid"]:
self.__user.listenbrainz_session = token
self.__user.listenbrainz_status = True
self.__user.save()
return True, "OK"
else:
return False, f"Error: {res['message']}"
def unlink_account(self):
self.__user.listenbrainz_session = None
self.__user.listenbrainz_status = True
self.__user.save()
def now_playing(self, track):
if not self.__enabled:
return
self.__api_request(
True,
"/1/submit-listens",
self.__user.listenbrainz_session,
listen_type="playing_now",
payload=[{
"track_metadata": {
"artist_name": track.album.artist.name,
"track_name": track.title,
"release_name": track.album.name,
"additional_info": {
"media_player": "Supysonic",
"duration_ms": track.duration,
},
},
}]
)
def scrobble(self, track, ts):
if not self.__enabled:
return
self.__api_request(
True,
"/1/submit-listens",
self.__user.listenbrainz_session,
listen_type="single",
payload=[{
"listened_at": ts,
"track_metadata": {
"artist_name": track.album.artist.name,
"track_name": track.title,
"release_name": track.album.name,
"additional_info": {
"media_player": "Supysonic",
"duration_ms": track.duration,
},
},
}]
)
def __api_request(self, write, route, token, **kwargs):
if not self.__enabled or not token:
return
headers = {"Content-Type": "application/json"}
headers["Authorization"] = "Token {0}".format(token)
try:
if write:
r = requests.post(
urljoin(self.__api_url, route),
headers=headers,
data=json.dumps(kwargs),
timeout=5)
else:
r = requests.get(
urljoin(self.__api_url, route),
headers=headers,
data=json.dumps(kwargs),
timeout=5)
r.raise_for_status()
except requests.HTTPError as e:
status_code = e.response.status_code
if status_code == 401: # Unauthorized
self.__user.listenbrainz_status = False
self.__user.save()
message = e.response.json().get("error", "")
logger.warning("ListenBrainz error %i: %s", status_code, message)
return None
except requests.exceptions.RequestException as e:
logger.warning("Error while connecting to ListenBrainz: " + str(e))
return None
return r.json()

View File

@ -0,0 +1,2 @@
ALTER TABLE user ADD listenbrainz_session CHAR(36);
ALTER TABLE user ADD listenbrainz_status BOOLEAN NOT NULL DEFAULT TRUE;

View File

@ -0,0 +1,2 @@
ALTER TABLE user ADD COLUMN listenbrainz_session CHAR(36);
ALTER TABLE user ADD COLUMN listenbrainz_status BOOLEAN NOT NULL DEFAULT TRUE;

View File

@ -0,0 +1,2 @@
ALTER TABLE user ADD listenbrainz_session CHAR(36);
ALTER TABLE user ADD listenbrainz_status BOOLEAN NOT NULL DEFAULT TRUE;

View File

@ -57,6 +57,8 @@ CREATE TABLE IF NOT EXISTS user (
salt CHAR(6) NOT NULL,
admin BOOLEAN NOT NULL,
jukebox BOOLEAN NOT NULL,
listenbrainz_session CHAR(36),
listenbrainz_status BOOLEAN NOT NULL,
lastfm_session CHAR(32),
lastfm_status BOOLEAN NOT NULL,
last_play_id CHAR(32) REFERENCES track(id),

View File

@ -57,6 +57,8 @@ CREATE TABLE IF NOT EXISTS "user" (
salt CHAR(6) NOT NULL,
admin BOOLEAN NOT NULL,
jukebox BOOLEAN NOT NULL,
listenbrainz_session CHAR(36),
listenbrainz_status BOOLEAN NOT NULL,
lastfm_session CHAR(32),
lastfm_status BOOLEAN NOT NULL,
last_play_id UUID REFERENCES track,

View File

@ -59,6 +59,8 @@ CREATE TABLE IF NOT EXISTS user (
salt CHAR(6) NOT NULL,
admin BOOLEAN NOT NULL,
jukebox BOOLEAN NOT NULL,
listenbrainz_session CHAR(36),
listenbrainz_status BOOLEAN NOT NULL,
lastfm_session CHAR(32),
lastfm_status BOOLEAN NOT NULL,
last_play_id CHAR(36) REFERENCES track,

View File

@ -81,6 +81,28 @@
</div>
</form>
</div>
<div class="col-md-6">
<form>
<div class="form_group">
<label class="sr-only" for="listenbrainz">ListenBrainz status</label>
<div class="input-group">
<div class="input-group-addon">ListenBrainz status</div>
{% if user.listenbrainz_session %}
<input class="form-control" type="text" id="listenbrainz" placeholder="{% if user.listenbrainz_status %}Linked{% else %}Invalid token{% endif %}" readonly>
<div class="input-group-btn">
<a class="btn btn-default" href="{{ url_for('frontend.listenbrainz_unreg', uid = user.id) }}">Unlink</a>
</div>
{% else %}
<input class="form-control" type="text" name="token" id="listenbrainz" placeholder="Insert auth token" maxlength="36" />
<div class="input-group-btn">
<button class="btn btn-default" type="submit" formaction="{{ url_for('frontend.listenbrainz_reg', uid = user.id) }}">Link</button>
</div>
{% endif %}
</div>
</div>
</form>
<br>
</div>
</div>
{% if request.user.id == user.id %}
<a href="{{ url_for('frontend.change_password_form', uid = 'me') }}" class="btn btn-default">Change password</a></li>