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:
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 StarredTrack, StarredAlbum, StarredArtist, StarredFolder
|
||||||
from ..db import RatingTrack, RatingFolder
|
from ..db import RatingTrack, RatingFolder
|
||||||
from ..lastfm import LastFm
|
from ..lastfm import LastFm
|
||||||
|
from ..listenbrainz import ListenBrainz
|
||||||
|
|
||||||
from . import get_entity, get_entity_id, api_routing
|
from . import get_entity, get_entity_id, api_routing
|
||||||
from .exceptions import AggregateException, GenericError, MissingParameter, NotFound
|
from .exceptions import AggregateException, GenericError, MissingParameter, NotFound
|
||||||
@ -175,10 +176,13 @@ def scrobble():
|
|||||||
t = int(t) / 1000 if t else int(time.time())
|
t = int(t) / 1000 if t else int(time.time())
|
||||||
|
|
||||||
lfm = LastFm(current_app.config["LASTFM"], request.user)
|
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"):
|
if submission in (None, "", True, "true", "True", 1, "1"):
|
||||||
lfm.scrobble(res, t)
|
lfm.scrobble(res, t)
|
||||||
|
lbz.scrobble(res, t)
|
||||||
else:
|
else:
|
||||||
lfm.now_playing(res)
|
lfm.now_playing(res)
|
||||||
|
lbz.now_playing(res)
|
||||||
|
|
||||||
return request.formatter.empty
|
return request.formatter.empty
|
||||||
|
@ -52,6 +52,7 @@ class DefaultConfig:
|
|||||||
"log_rotate": True,
|
"log_rotate": True,
|
||||||
}
|
}
|
||||||
LASTFM = {"api_key": None, "secret": None}
|
LASTFM = {"api_key": None, "secret": None}
|
||||||
|
LISTENBRAINZ = {"api_url": "https://api.listenbrainz.org"}
|
||||||
TRANSCODING = {}
|
TRANSCODING = {}
|
||||||
MIMETYPES = {}
|
MIMETYPES = {}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ from playhouse.db_url import parseresult_to_dict, schemes
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
SCHEMA_VERSION = "20230331"
|
SCHEMA_VERSION = "20240318"
|
||||||
|
|
||||||
|
|
||||||
def now():
|
def now():
|
||||||
@ -439,6 +439,11 @@ class User(_Model):
|
|||||||
default=True
|
default=True
|
||||||
) # True: ok/unlinked, False: invalid session
|
) # 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 = ForeignKeyField(Track, null=True, backref="+")
|
||||||
last_play_date = DateTimeField(null=True)
|
last_play_date = DateTimeField(null=True)
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from functools import wraps
|
|||||||
|
|
||||||
from ..db import ClientPrefs, User
|
from ..db import ClientPrefs, User
|
||||||
from ..lastfm import LastFm
|
from ..lastfm import LastFm
|
||||||
|
from ..listenbrainz import ListenBrainz
|
||||||
from ..managers.user import UserManager
|
from ..managers.user import UserManager
|
||||||
|
|
||||||
from . import admin_only, frontend
|
from . import admin_only, frontend
|
||||||
@ -297,6 +298,30 @@ def lastfm_unreg(uid, user):
|
|||||||
return redirect(url_for("frontend.user_profile", uid=uid))
|
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"])
|
@frontend.route("/user/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
return_url = request.args.get("returnUrl") or url_for("frontend.index")
|
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,
|
salt CHAR(6) NOT NULL,
|
||||||
admin BOOLEAN NOT NULL,
|
admin BOOLEAN NOT NULL,
|
||||||
jukebox BOOLEAN NOT NULL,
|
jukebox BOOLEAN NOT NULL,
|
||||||
|
listenbrainz_session CHAR(36),
|
||||||
|
listenbrainz_status BOOLEAN NOT NULL,
|
||||||
lastfm_session CHAR(32),
|
lastfm_session CHAR(32),
|
||||||
lastfm_status BOOLEAN NOT NULL,
|
lastfm_status BOOLEAN NOT NULL,
|
||||||
last_play_id CHAR(32) REFERENCES track(id),
|
last_play_id CHAR(32) REFERENCES track(id),
|
||||||
|
@ -57,6 +57,8 @@ CREATE TABLE IF NOT EXISTS "user" (
|
|||||||
salt CHAR(6) NOT NULL,
|
salt CHAR(6) NOT NULL,
|
||||||
admin BOOLEAN NOT NULL,
|
admin BOOLEAN NOT NULL,
|
||||||
jukebox BOOLEAN NOT NULL,
|
jukebox BOOLEAN NOT NULL,
|
||||||
|
listenbrainz_session CHAR(36),
|
||||||
|
listenbrainz_status BOOLEAN NOT NULL,
|
||||||
lastfm_session CHAR(32),
|
lastfm_session CHAR(32),
|
||||||
lastfm_status BOOLEAN NOT NULL,
|
lastfm_status BOOLEAN NOT NULL,
|
||||||
last_play_id UUID REFERENCES track,
|
last_play_id UUID REFERENCES track,
|
||||||
|
@ -59,6 +59,8 @@ CREATE TABLE IF NOT EXISTS user (
|
|||||||
salt CHAR(6) NOT NULL,
|
salt CHAR(6) NOT NULL,
|
||||||
admin BOOLEAN NOT NULL,
|
admin BOOLEAN NOT NULL,
|
||||||
jukebox BOOLEAN NOT NULL,
|
jukebox BOOLEAN NOT NULL,
|
||||||
|
listenbrainz_session CHAR(36),
|
||||||
|
listenbrainz_status BOOLEAN NOT NULL,
|
||||||
lastfm_session CHAR(32),
|
lastfm_session CHAR(32),
|
||||||
lastfm_status BOOLEAN NOT NULL,
|
lastfm_status BOOLEAN NOT NULL,
|
||||||
last_play_id CHAR(36) REFERENCES track,
|
last_play_id CHAR(36) REFERENCES track,
|
||||||
|
@ -81,6 +81,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{% if request.user.id == user.id %}
|
{% if request.user.id == user.id %}
|
||||||
<a href="{{ url_for('frontend.change_password_form', uid = 'me') }}" class="btn btn-default">Change password</a></li>
|
<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