mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 00:46:18 +00:00
Initial ListenBrainz support
This commit is contained in:
parent
1feaae7637
commit
a39f2fb16a
@ -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
|
||||
|
@ -52,6 +52,7 @@ class DefaultConfig:
|
||||
"log_rotate": True,
|
||||
}
|
||||
LASTFM = {"api_key": None, "secret": None}
|
||||
LISTENBRAINZ = {"api_url": "https://api.listenbrainz.org"}
|
||||
TRANSCODING = {}
|
||||
MIMETYPES = {}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
127
supysonic/listenbrainz.py
Normal 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()
|
2
supysonic/schema/migration/mysql/20240318.sql
Normal file
2
supysonic/schema/migration/mysql/20240318.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE user ADD listenbrainz_session CHAR(36);
|
||||
ALTER TABLE user ADD listenbrainz_status BOOLEAN NOT NULL DEFAULT TRUE;
|
2
supysonic/schema/migration/postgres/20240318.sql
Normal file
2
supysonic/schema/migration/postgres/20240318.sql
Normal 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;
|
2
supysonic/schema/migration/sqlite/20240318.sql
Normal file
2
supysonic/schema/migration/sqlite/20240318.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE user ADD listenbrainz_session CHAR(36);
|
||||
ALTER TABLE user ADD listenbrainz_status BOOLEAN NOT NULL DEFAULT TRUE;
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user