2019-10-15 09:38:21 +00:00
|
|
|
"""
|
|
|
|
Documentation of Subsonic API can be found at http://www.subsonic.org/pages/api.jsp
|
|
|
|
"""
|
2018-05-08 19:22:52 +00:00
|
|
|
import datetime
|
2019-01-11 12:33:35 +00:00
|
|
|
import functools
|
2018-05-08 19:22:52 +00:00
|
|
|
|
2018-05-31 21:46:15 +00:00
|
|
|
from django.conf import settings
|
2019-07-19 06:19:36 +00:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2020-03-18 14:52:23 +00:00
|
|
|
from django.db import transaction
|
|
|
|
from django.db.models import Count, Prefetch, Q
|
2018-05-08 19:22:52 +00:00
|
|
|
from django.utils import timezone
|
2018-05-08 14:32:07 +00:00
|
|
|
from rest_framework import exceptions
|
|
|
|
from rest_framework import permissions as rest_permissions
|
2018-06-10 08:55:16 +00:00
|
|
|
from rest_framework import renderers, response, viewsets
|
2019-01-11 12:33:35 +00:00
|
|
|
from rest_framework.decorators import action
|
2018-05-08 14:32:07 +00:00
|
|
|
from rest_framework.serializers import ValidationError
|
|
|
|
|
2018-10-21 18:22:50 +00:00
|
|
|
import funkwhale_api
|
2018-06-01 22:11:31 +00:00
|
|
|
from funkwhale_api.activity import record
|
2020-03-18 14:52:23 +00:00
|
|
|
from funkwhale_api.audio import models as audio_models
|
|
|
|
from funkwhale_api.audio import serializers as audio_serializers
|
|
|
|
from funkwhale_api.audio import views as audio_views
|
2019-11-25 08:49:06 +00:00
|
|
|
from funkwhale_api.common import (
|
|
|
|
fields,
|
|
|
|
preferences,
|
2020-03-18 14:52:23 +00:00
|
|
|
models as common_models,
|
2019-11-25 08:49:06 +00:00
|
|
|
utils as common_utils,
|
|
|
|
tasks as common_tasks,
|
|
|
|
)
|
2020-03-18 14:52:23 +00:00
|
|
|
from funkwhale_api.federation import models as federation_models
|
2018-05-08 19:22:52 +00:00
|
|
|
from funkwhale_api.favorites.models import TrackFavorite
|
2019-02-14 09:49:06 +00:00
|
|
|
from funkwhale_api.moderation import filters as moderation_filters
|
2018-05-08 14:32:07 +00:00
|
|
|
from funkwhale_api.music import models as music_models
|
2020-03-11 12:59:31 +00:00
|
|
|
from funkwhale_api.music import serializers as music_serializers
|
2018-05-08 19:22:52 +00:00
|
|
|
from funkwhale_api.music import utils
|
2018-05-08 14:32:07 +00:00
|
|
|
from funkwhale_api.music import views as music_views
|
2018-05-08 21:06:47 +00:00
|
|
|
from funkwhale_api.playlists import models as playlists_models
|
2019-07-19 06:19:36 +00:00
|
|
|
from funkwhale_api.tags import models as tags_models
|
2018-10-21 16:42:59 +00:00
|
|
|
from funkwhale_api.users import models as users_models
|
2018-05-08 14:32:07 +00:00
|
|
|
|
2018-06-10 08:55:16 +00:00
|
|
|
from . import authentication, filters, negotiation, serializers
|
2018-05-08 14:32:07 +00:00
|
|
|
|
|
|
|
|
2018-10-01 19:16:11 +00:00
|
|
|
def find_object(
|
|
|
|
queryset, model_field="pk", field="id", cast=int, filter_playable=False
|
|
|
|
):
|
2018-05-08 14:32:07 +00:00
|
|
|
def decorator(func):
|
2019-01-11 12:33:35 +00:00
|
|
|
@functools.wraps(func)
|
2018-05-08 14:32:07 +00:00
|
|
|
def inner(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
try:
|
|
|
|
raw_value = data[field]
|
|
|
|
except KeyError:
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"code": 10,
|
|
|
|
"message": "required parameter '{}' not present".format(
|
|
|
|
field
|
|
|
|
),
|
|
|
|
}
|
2018-05-10 15:31:49 +00:00
|
|
|
}
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 14:32:07 +00:00
|
|
|
try:
|
|
|
|
value = cast(raw_value)
|
2018-08-30 12:01:44 +00:00
|
|
|
except (ValueError, TypeError, ValidationError):
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"code": 0,
|
|
|
|
"message": 'For input string "{}"'.format(raw_value),
|
|
|
|
}
|
2018-05-10 15:31:49 +00:00
|
|
|
}
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 21:06:47 +00:00
|
|
|
qs = queryset
|
2018-06-09 13:36:16 +00:00
|
|
|
if hasattr(qs, "__call__"):
|
2018-05-08 21:06:47 +00:00
|
|
|
qs = qs(request)
|
2018-10-01 19:16:11 +00:00
|
|
|
|
|
|
|
if filter_playable:
|
|
|
|
actor = utils.get_actor_from_request(request)
|
2019-01-03 17:59:31 +00:00
|
|
|
qs = qs.playable_by(actor)
|
2018-10-01 19:16:11 +00:00
|
|
|
|
2018-05-08 14:32:07 +00:00
|
|
|
try:
|
2018-05-08 21:06:47 +00:00
|
|
|
obj = qs.get(**{model_field: value})
|
|
|
|
except qs.model.DoesNotExist:
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"code": 70,
|
2019-01-25 11:11:34 +00:00
|
|
|
"message": "{} not found".format(qs.model.__name__),
|
2018-06-09 13:36:16 +00:00
|
|
|
}
|
2018-05-10 15:31:49 +00:00
|
|
|
}
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
|
|
|
kwargs["obj"] = obj
|
2018-05-08 14:32:07 +00:00
|
|
|
return func(self, request, *args, **kwargs)
|
2018-06-09 13:36:16 +00:00
|
|
|
|
2018-05-08 14:32:07 +00:00
|
|
|
return inner
|
2018-06-09 13:36:16 +00:00
|
|
|
|
2018-05-08 14:32:07 +00:00
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2019-01-25 11:11:34 +00:00
|
|
|
def get_playlist_qs(request):
|
|
|
|
qs = playlists_models.Playlist.objects.filter(
|
|
|
|
fields.privacy_level_query(request.user)
|
|
|
|
)
|
|
|
|
qs = qs.with_tracks_count().exclude(_tracks_count=0).select_related("user")
|
|
|
|
return qs.order_by("-creation_date")
|
|
|
|
|
|
|
|
|
2020-03-18 14:52:23 +00:00
|
|
|
def requires_channels(f):
|
|
|
|
@functools.wraps(f)
|
|
|
|
def inner(*args, **kwargs):
|
|
|
|
if not preferences.get("audio__channels_enabled"):
|
|
|
|
payload = {
|
|
|
|
"error": {
|
|
|
|
"code": 0,
|
|
|
|
"message": "Channels / podcasts are disabled on this pod",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return response.Response(payload, status=405)
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
2018-05-08 14:32:07 +00:00
|
|
|
class SubsonicViewSet(viewsets.GenericViewSet):
|
|
|
|
content_negotiation_class = negotiation.SubsonicContentNegociation
|
|
|
|
authentication_classes = [authentication.SubsonicAuthentication]
|
2019-03-25 16:02:51 +00:00
|
|
|
permission_classes = [rest_permissions.IsAuthenticated]
|
2020-01-06 08:55:58 +00:00
|
|
|
throttling_scopes = {"*": {"authenticated": "subsonic", "anonymous": "subsonic"}}
|
2018-05-08 14:32:07 +00:00
|
|
|
|
2018-05-09 20:13:04 +00:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
if not preferences.get("subsonic__enabled"):
|
2018-05-09 20:13:04 +00:00
|
|
|
r = response.Response({}, status=405)
|
|
|
|
r.accepted_renderer = renderers.JSONRenderer()
|
2018-06-09 13:36:16 +00:00
|
|
|
r.accepted_media_type = "application/json"
|
2018-05-09 20:13:04 +00:00
|
|
|
r.renderer_context = {}
|
|
|
|
return r
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
2018-05-08 14:32:07 +00:00
|
|
|
def handle_exception(self, exc):
|
|
|
|
# subsonic API sends 200 status code with custom error
|
|
|
|
# codes in the payload
|
2018-11-18 22:17:31 +00:00
|
|
|
mapping = {
|
|
|
|
exceptions.AuthenticationFailed: (40, "Wrong username or password."),
|
|
|
|
exceptions.NotAuthenticated: (10, "Required parameter is missing."),
|
|
|
|
}
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"status": "failed"}
|
2018-05-10 15:31:49 +00:00
|
|
|
if exc.__class__ in mapping:
|
2018-05-08 14:32:07 +00:00
|
|
|
code, message = mapping[exc.__class__]
|
|
|
|
else:
|
2018-05-10 15:31:49 +00:00
|
|
|
return super().handle_exception(exc)
|
2018-06-09 13:36:16 +00:00
|
|
|
payload["error"] = {"code": code, "message": message}
|
2018-05-08 14:32:07 +00:00
|
|
|
|
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(detail=False, methods=["get", "post"], permission_classes=[])
|
2018-05-08 14:32:07 +00:00
|
|
|
def ping(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"status": "ok", "version": "1.16.0"}
|
2018-05-08 14:32:07 +00:00
|
|
|
return response.Response(data, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
2018-06-09 13:36:16 +00:00
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_license",
|
2019-03-25 16:02:51 +00:00
|
|
|
permission_classes=[],
|
2018-06-09 13:36:16 +00:00
|
|
|
url_path="getLicense",
|
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
def get_license(self, request, *args, **kwargs):
|
|
|
|
now = timezone.now()
|
|
|
|
data = {
|
2018-06-09 13:36:16 +00:00
|
|
|
"status": "ok",
|
|
|
|
"version": "1.16.0",
|
2018-10-21 18:22:50 +00:00
|
|
|
"type": "funkwhale",
|
2018-10-21 18:43:12 +00:00
|
|
|
"funkwhaleVersion": funkwhale_api.__version__,
|
2018-06-09 13:36:16 +00:00
|
|
|
"license": {
|
|
|
|
"valid": "true",
|
|
|
|
"email": "valid@valid.license",
|
|
|
|
"licenseExpires": now + datetime.timedelta(days=365),
|
|
|
|
},
|
2018-05-08 19:22:52 +00:00
|
|
|
}
|
|
|
|
return response.Response(data, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_artists",
|
|
|
|
url_path="getArtists",
|
|
|
|
)
|
2018-05-08 14:32:07 +00:00
|
|
|
def get_artists(self, request, *args, **kwargs):
|
2019-02-14 09:49:06 +00:00
|
|
|
artists = (
|
|
|
|
music_models.Artist.objects.all()
|
|
|
|
.exclude(
|
|
|
|
moderation_filters.get_filtered_content_query(
|
|
|
|
moderation_filters.USER_FILTER_CONFIG["ARTIST"], request.user
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.playable_by(utils.get_actor_from_request(request))
|
2018-10-01 19:16:11 +00:00
|
|
|
)
|
2018-05-08 14:32:07 +00:00
|
|
|
data = serializers.GetArtistsSerializer(artists).data
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"artists": data}
|
2018-05-08 14:32:07 +00:00
|
|
|
|
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_indexes",
|
|
|
|
url_path="getIndexes",
|
|
|
|
)
|
2018-05-09 18:32:17 +00:00
|
|
|
def get_indexes(self, request, *args, **kwargs):
|
2019-02-14 09:49:06 +00:00
|
|
|
artists = (
|
|
|
|
music_models.Artist.objects.all()
|
|
|
|
.exclude(
|
|
|
|
moderation_filters.get_filtered_content_query(
|
|
|
|
moderation_filters.USER_FILTER_CONFIG["ARTIST"], request.user
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.playable_by(utils.get_actor_from_request(request))
|
2018-10-01 19:16:11 +00:00
|
|
|
)
|
2018-05-09 18:32:17 +00:00
|
|
|
data = serializers.GetArtistsSerializer(artists).data
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"indexes": data}
|
2018-05-09 18:32:17 +00:00
|
|
|
|
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_artist",
|
|
|
|
url_path="getArtist",
|
|
|
|
)
|
2018-10-01 19:16:11 +00:00
|
|
|
@find_object(music_models.Artist.objects.all(), filter_playable=True)
|
2018-05-08 14:32:07 +00:00
|
|
|
def get_artist(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
artist = kwargs.pop("obj")
|
2018-05-08 14:32:07 +00:00
|
|
|
data = serializers.GetArtistSerializer(artist).data
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"artist": data}
|
2018-05-08 14:32:07 +00:00
|
|
|
|
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False, methods=["get", "post"], url_name="get_song", url_path="getSong"
|
|
|
|
)
|
2018-10-01 19:16:11 +00:00
|
|
|
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
2018-08-30 12:04:41 +00:00
|
|
|
def get_song(self, request, *args, **kwargs):
|
|
|
|
track = kwargs.pop("obj")
|
|
|
|
data = serializers.GetSongSerializer(track).data
|
|
|
|
payload = {"song": data}
|
|
|
|
|
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_artist_info2",
|
|
|
|
url_path="getArtistInfo2",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-10-01 19:16:11 +00:00
|
|
|
@find_object(music_models.Artist.objects.all(), filter_playable=True)
|
2018-05-08 19:22:52 +00:00
|
|
|
def get_artist_info2(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"artist-info2": {}}
|
2018-05-08 19:22:52 +00:00
|
|
|
|
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False, methods=["get", "post"], url_name="get_album", url_path="getAlbum"
|
|
|
|
)
|
2018-10-01 19:16:11 +00:00
|
|
|
@find_object(
|
|
|
|
music_models.Album.objects.select_related("artist"), filter_playable=True
|
|
|
|
)
|
2018-05-08 14:32:07 +00:00
|
|
|
def get_album(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
album = kwargs.pop("obj")
|
2018-05-08 14:32:07 +00:00
|
|
|
data = serializers.GetAlbumSerializer(album).data
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"album": data}
|
2018-05-08 14:32:07 +00:00
|
|
|
return response.Response(payload, status=200)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream")
|
2018-10-01 19:16:11 +00:00
|
|
|
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
2018-05-08 14:32:07 +00:00
|
|
|
def stream(self, request, *args, **kwargs):
|
2018-10-26 13:31:33 +00:00
|
|
|
data = request.GET or request.POST
|
2018-06-09 13:36:16 +00:00
|
|
|
track = kwargs.pop("obj")
|
2018-09-22 12:29:30 +00:00
|
|
|
queryset = track.uploads.select_related("track__album__artist", "track__artist")
|
2020-03-11 12:59:31 +00:00
|
|
|
sorted_uploads = music_serializers.sort_uploads_for_listen(queryset)
|
|
|
|
|
|
|
|
if not sorted_uploads:
|
2018-05-08 19:22:52 +00:00
|
|
|
return response.Response(status=404)
|
2018-10-26 13:31:33 +00:00
|
|
|
|
2020-03-11 12:59:31 +00:00
|
|
|
upload = sorted_uploads[0]
|
|
|
|
|
2019-04-16 09:35:12 +00:00
|
|
|
max_bitrate = data.get("maxBitRate")
|
|
|
|
try:
|
|
|
|
max_bitrate = min(max(int(max_bitrate), 0), 320) or None
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
max_bitrate = None
|
|
|
|
|
|
|
|
if max_bitrate:
|
|
|
|
max_bitrate = max_bitrate * 1000
|
2019-06-28 07:59:57 +00:00
|
|
|
|
2019-07-03 09:35:34 +00:00
|
|
|
format = data.get("format") or None
|
2019-06-28 07:59:57 +00:00
|
|
|
if max_bitrate and not format:
|
|
|
|
# specific bitrate requested, but no format specified
|
|
|
|
# so we use a default one, cf #867. This helps with clients
|
|
|
|
# that don't send the format parameter, such as DSub.
|
|
|
|
format = settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT
|
|
|
|
elif format == "raw":
|
|
|
|
format = None
|
|
|
|
|
2019-04-16 09:35:12 +00:00
|
|
|
return music_views.handle_serve(
|
2019-05-09 09:43:35 +00:00
|
|
|
upload=upload,
|
|
|
|
user=request.user,
|
|
|
|
format=format,
|
|
|
|
max_bitrate=max_bitrate,
|
|
|
|
# Subsonic clients don't expect 302 redirection unfortunately,
|
|
|
|
# So we have to proxy media files
|
|
|
|
proxy_media=True,
|
2020-01-20 11:13:02 +00:00
|
|
|
wsgi_request=request._request,
|
2019-04-16 09:35:12 +00:00
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(detail=False, methods=["get", "post"], url_name="star", url_path="star")
|
2018-06-09 13:36:16 +00:00
|
|
|
@find_object(music_models.Track.objects.all())
|
2018-05-08 19:22:52 +00:00
|
|
|
def star(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
track = kwargs.pop("obj")
|
2018-05-08 19:22:52 +00:00
|
|
|
TrackFavorite.add(user=request.user, track=track)
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response({"status": "ok"})
|
2018-05-08 19:22:52 +00:00
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(detail=False, methods=["get", "post"], url_name="unstar", url_path="unstar")
|
2018-06-09 13:36:16 +00:00
|
|
|
@find_object(music_models.Track.objects.all())
|
2018-05-08 19:22:52 +00:00
|
|
|
def unstar(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
track = kwargs.pop("obj")
|
2018-05-08 19:22:52 +00:00
|
|
|
request.user.track_favorites.filter(track=track).delete()
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response({"status": "ok"})
|
2018-05-08 19:22:52 +00:00
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_starred2",
|
|
|
|
url_path="getStarred2",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
def get_starred2(self, request, *args, **kwargs):
|
|
|
|
favorites = request.user.track_favorites.all()
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
|
2018-05-09 18:32:17 +00:00
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_random_songs",
|
|
|
|
url_path="getRandomSongs",
|
2018-10-21 18:22:31 +00:00
|
|
|
)
|
|
|
|
def get_random_songs(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
actor = utils.get_actor_from_request(request)
|
2019-02-14 09:49:06 +00:00
|
|
|
queryset = music_models.Track.objects.all().exclude(
|
|
|
|
moderation_filters.get_filtered_content_query(
|
|
|
|
moderation_filters.USER_FILTER_CONFIG["TRACK"], request.user
|
|
|
|
)
|
|
|
|
)
|
2018-10-21 18:22:31 +00:00
|
|
|
queryset = queryset.playable_by(actor)
|
|
|
|
try:
|
|
|
|
size = int(data["size"])
|
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
size = 50
|
|
|
|
|
2018-10-21 18:33:41 +00:00
|
|
|
queryset = (
|
|
|
|
queryset.playable_by(actor).prefetch_related("uploads").order_by("?")[:size]
|
|
|
|
)
|
2018-10-21 18:22:31 +00:00
|
|
|
data = {
|
|
|
|
"randomSongs": {
|
|
|
|
"song": serializers.GetSongSerializer(queryset, many=True).data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return response.Response(data)
|
|
|
|
|
2019-09-19 19:09:18 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_songs_by_genre",
|
|
|
|
url_path="getSongsByGenre",
|
|
|
|
)
|
|
|
|
def get_songs_by_genre(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
actor = utils.get_actor_from_request(request)
|
|
|
|
queryset = music_models.Track.objects.all().exclude(
|
|
|
|
moderation_filters.get_filtered_content_query(
|
|
|
|
moderation_filters.USER_FILTER_CONFIG["TRACK"], request.user
|
|
|
|
)
|
|
|
|
)
|
|
|
|
queryset = queryset.playable_by(actor)
|
2019-10-28 07:58:32 +00:00
|
|
|
try:
|
|
|
|
offset = int(data.get("offset", 0))
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
|
|
offset = 0
|
|
|
|
|
2019-09-19 19:09:18 +00:00
|
|
|
try:
|
|
|
|
size = int(
|
|
|
|
data["count"]
|
|
|
|
) # yep. Some endpoints have size, other have count…
|
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
size = 50
|
|
|
|
|
|
|
|
genre = data.get("genre")
|
|
|
|
queryset = (
|
|
|
|
queryset.playable_by(actor)
|
|
|
|
.filter(
|
|
|
|
Q(tagged_items__tag__name=genre)
|
|
|
|
| Q(artist__tagged_items__tag__name=genre)
|
|
|
|
| Q(album__artist__tagged_items__tag__name=genre)
|
|
|
|
| Q(album__tagged_items__tag__name=genre)
|
|
|
|
)
|
|
|
|
.prefetch_related("uploads")
|
|
|
|
.distinct()
|
2019-10-28 07:58:32 +00:00
|
|
|
.order_by("-creation_date")[offset : offset + size]
|
2019-09-19 19:09:18 +00:00
|
|
|
)
|
|
|
|
data = {
|
|
|
|
"songsByGenre": {
|
|
|
|
"song": serializers.GetSongSerializer(queryset, many=True).data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_starred",
|
|
|
|
url_path="getStarred",
|
|
|
|
)
|
2018-05-09 18:32:17 +00:00
|
|
|
def get_starred(self, request, *args, **kwargs):
|
|
|
|
favorites = request.user.track_favorites.all()
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"starred": {"song": serializers.get_starred_tracks_data(favorites)}}
|
2018-05-08 19:22:52 +00:00
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_album_list2",
|
|
|
|
url_path="getAlbumList2",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
def get_album_list2(self, request, *args, **kwargs):
|
2019-02-14 09:49:06 +00:00
|
|
|
queryset = (
|
|
|
|
music_models.Album.objects.exclude(
|
|
|
|
moderation_filters.get_filtered_content_query(
|
|
|
|
moderation_filters.USER_FILTER_CONFIG["ALBUM"], request.user
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.with_tracks_count()
|
|
|
|
.order_by("artist__name")
|
2018-06-10 11:34:15 +00:00
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
data = request.GET or request.POST
|
|
|
|
filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
|
|
|
|
queryset = filterset.qs
|
2018-10-01 19:16:11 +00:00
|
|
|
actor = utils.get_actor_from_request(request)
|
|
|
|
queryset = queryset.playable_by(actor)
|
2019-07-19 06:19:36 +00:00
|
|
|
type = data.get("type", "alphabeticalByArtist")
|
|
|
|
|
|
|
|
if type == "alphabeticalByArtist":
|
|
|
|
queryset = queryset.order_by("artist__name")
|
|
|
|
elif type == "random":
|
|
|
|
queryset = queryset.order_by("?")
|
|
|
|
elif type == "alphabeticalByName" or not type:
|
|
|
|
queryset = queryset.order_by("artist__title")
|
|
|
|
elif type == "recent" or not type:
|
2020-06-11 16:06:00 +00:00
|
|
|
queryset = queryset.exclude(release_date=None).order_by("-release_date")
|
2019-07-19 06:19:36 +00:00
|
|
|
elif type == "newest" or not type:
|
|
|
|
queryset = queryset.order_by("-creation_date")
|
|
|
|
elif type == "byGenre" and data.get("genre"):
|
|
|
|
genre = data.get("genre")
|
|
|
|
queryset = queryset.filter(
|
|
|
|
Q(tagged_items__tag__name=genre)
|
|
|
|
| Q(artist__tagged_items__tag__name=genre)
|
|
|
|
)
|
2019-10-15 09:38:21 +00:00
|
|
|
elif type == "byYear":
|
|
|
|
try:
|
|
|
|
boundaries = [
|
|
|
|
int(data.get("fromYear", 0)),
|
|
|
|
int(data.get("toYear", 99999999)),
|
|
|
|
]
|
2018-10-01 19:16:11 +00:00
|
|
|
|
2019-10-15 09:38:21 +00:00
|
|
|
except (TypeError, ValueError):
|
|
|
|
return response.Response(
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"code": 10,
|
|
|
|
"message": "Invalid fromYear or toYear parameter",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
# because, yeah, the specification explicitly state that fromYear can be greater
|
|
|
|
# than toYear, to indicate reverse ordering…
|
|
|
|
# http://www.subsonic.org/pages/api.jsp#getAlbumList2
|
|
|
|
from_year = min(boundaries)
|
|
|
|
to_year = max(boundaries)
|
|
|
|
queryset = queryset.filter(
|
|
|
|
release_date__year__gte=from_year, release_date__year__lte=to_year
|
|
|
|
)
|
|
|
|
if boundaries[0] <= boundaries[1]:
|
|
|
|
queryset = queryset.order_by("release_date")
|
|
|
|
else:
|
|
|
|
queryset = queryset.order_by("-release_date")
|
2018-05-08 19:22:52 +00:00
|
|
|
try:
|
2018-06-09 13:36:16 +00:00
|
|
|
offset = int(data["offset"])
|
2018-05-08 19:22:52 +00:00
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
offset = 0
|
|
|
|
|
|
|
|
try:
|
2018-06-09 13:36:16 +00:00
|
|
|
size = int(data["size"])
|
2018-05-08 19:22:52 +00:00
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
size = 50
|
|
|
|
|
|
|
|
size = min(size, 500)
|
2018-06-10 11:34:15 +00:00
|
|
|
queryset = queryset[offset : offset + size]
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}}
|
2018-05-08 19:22:52 +00:00
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False, methods=["get", "post"], url_name="search3", url_path="search3"
|
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
def search3(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
2018-06-09 13:36:16 +00:00
|
|
|
query = str(data.get("query", "")).replace("*", "")
|
2018-10-01 19:16:11 +00:00
|
|
|
actor = utils.get_actor_from_request(request)
|
2018-05-08 19:22:52 +00:00
|
|
|
conf = [
|
|
|
|
{
|
2018-06-09 13:36:16 +00:00
|
|
|
"subsonic": "artist",
|
|
|
|
"search_fields": ["name"],
|
|
|
|
"queryset": (
|
|
|
|
music_models.Artist.objects.with_albums_count().values(
|
|
|
|
"id", "_albums_count", "name"
|
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
),
|
2018-06-09 13:36:16 +00:00
|
|
|
"serializer": lambda qs: [serializers.get_artist_data(a) for a in qs],
|
2018-05-08 19:22:52 +00:00
|
|
|
},
|
|
|
|
{
|
2018-06-09 13:36:16 +00:00
|
|
|
"subsonic": "album",
|
|
|
|
"search_fields": ["title"],
|
|
|
|
"queryset": (
|
|
|
|
music_models.Album.objects.with_tracks_count().select_related(
|
|
|
|
"artist"
|
|
|
|
)
|
2018-05-08 19:22:52 +00:00
|
|
|
),
|
2018-06-09 13:36:16 +00:00
|
|
|
"serializer": serializers.get_album_list2_data,
|
2018-05-08 19:22:52 +00:00
|
|
|
},
|
|
|
|
{
|
2018-06-09 13:36:16 +00:00
|
|
|
"subsonic": "song",
|
|
|
|
"search_fields": ["title"],
|
|
|
|
"queryset": (
|
2018-09-22 12:29:30 +00:00
|
|
|
music_models.Track.objects.prefetch_related(
|
|
|
|
"uploads"
|
|
|
|
).select_related("album__artist")
|
2018-05-08 19:22:52 +00:00
|
|
|
),
|
2018-06-09 13:36:16 +00:00
|
|
|
"serializer": serializers.get_song_list_data,
|
2018-05-08 19:22:52 +00:00
|
|
|
},
|
|
|
|
]
|
2018-06-09 13:36:16 +00:00
|
|
|
payload = {"searchResult3": {}}
|
2018-05-08 19:22:52 +00:00
|
|
|
for c in conf:
|
2018-06-09 13:36:16 +00:00
|
|
|
offsetKey = "{}Offset".format(c["subsonic"])
|
|
|
|
countKey = "{}Count".format(c["subsonic"])
|
2018-05-08 19:22:52 +00:00
|
|
|
try:
|
|
|
|
offset = int(data[offsetKey])
|
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
offset = 0
|
|
|
|
|
|
|
|
try:
|
|
|
|
size = int(data[countKey])
|
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
size = 20
|
|
|
|
|
|
|
|
size = min(size, 100)
|
2018-06-09 13:36:16 +00:00
|
|
|
queryset = c["queryset"]
|
2018-05-08 19:22:52 +00:00
|
|
|
if query:
|
2018-06-09 13:36:16 +00:00
|
|
|
queryset = c["queryset"].filter(
|
|
|
|
utils.get_query(query, c["search_fields"])
|
2018-05-08 19:22:52 +00:00
|
|
|
)
|
2018-10-01 19:16:11 +00:00
|
|
|
queryset = queryset.playable_by(actor)
|
2019-01-05 15:07:58 +00:00
|
|
|
queryset = common_utils.order_for_search(queryset, c["search_fields"][0])
|
2018-06-10 11:34:15 +00:00
|
|
|
queryset = queryset[offset : offset + size]
|
2018-06-09 13:36:16 +00:00
|
|
|
payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
|
2018-05-08 19:22:52 +00:00
|
|
|
return response.Response(payload)
|
2018-05-08 21:06:47 +00:00
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_playlists",
|
|
|
|
url_path="getPlaylists",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 21:06:47 +00:00
|
|
|
def get_playlists(self, request, *args, **kwargs):
|
2019-01-25 11:11:34 +00:00
|
|
|
qs = get_playlist_qs(request)
|
2018-05-08 21:06:47 +00:00
|
|
|
data = {
|
2019-01-25 11:11:34 +00:00
|
|
|
"playlists": {"playlist": [serializers.get_playlist_data(p) for p in qs]}
|
2018-05-08 21:06:47 +00:00
|
|
|
}
|
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_playlist",
|
|
|
|
url_path="getPlaylist",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2019-01-25 11:11:34 +00:00
|
|
|
@find_object(lambda request: get_playlist_qs(request))
|
2018-05-08 21:06:47 +00:00
|
|
|
def get_playlist(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
playlist = kwargs.pop("obj")
|
|
|
|
data = {"playlist": serializers.get_playlist_detail_data(playlist)}
|
2018-05-08 21:06:47 +00:00
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="update_playlist",
|
|
|
|
url_path="updatePlaylist",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
|
|
|
@find_object(lambda request: request.user.playlists.all(), field="playlistId")
|
2018-05-08 21:06:47 +00:00
|
|
|
def update_playlist(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
playlist = kwargs.pop("obj")
|
2018-05-08 21:06:47 +00:00
|
|
|
data = request.GET or request.POST
|
2018-06-09 13:36:16 +00:00
|
|
|
new_name = data.get("name", "")
|
2018-05-08 21:06:47 +00:00
|
|
|
if new_name:
|
|
|
|
playlist.name = new_name
|
2018-06-09 13:36:16 +00:00
|
|
|
playlist.save(update_fields=["name", "modification_date"])
|
2018-05-08 21:06:47 +00:00
|
|
|
try:
|
2018-06-09 13:36:16 +00:00
|
|
|
to_remove = int(data["songIndexToRemove"])
|
2018-05-08 21:06:47 +00:00
|
|
|
plt = playlist.playlist_tracks.get(index=to_remove)
|
|
|
|
except (TypeError, ValueError, KeyError):
|
|
|
|
pass
|
|
|
|
except playlists_models.PlaylistTrack.DoesNotExist:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
plt.delete(update_indexes=True)
|
|
|
|
|
2018-05-09 18:31:47 +00:00
|
|
|
ids = []
|
2018-06-09 13:36:16 +00:00
|
|
|
for i in data.getlist("songIdToAdd"):
|
2018-05-09 18:31:47 +00:00
|
|
|
try:
|
|
|
|
ids.append(int(i))
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
pass
|
|
|
|
if ids:
|
|
|
|
tracks = music_models.Track.objects.filter(pk__in=ids)
|
|
|
|
by_id = {t.id: t for t in tracks}
|
|
|
|
sorted_tracks = []
|
|
|
|
for i in ids:
|
|
|
|
try:
|
|
|
|
sorted_tracks.append(by_id[i])
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
if sorted_tracks:
|
|
|
|
playlist.insert_many(sorted_tracks)
|
|
|
|
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"status": "ok"}
|
2018-05-08 21:06:47 +00:00
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="delete_playlist",
|
|
|
|
url_path="deletePlaylist",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
|
|
|
@find_object(lambda request: request.user.playlists.all())
|
2018-05-08 21:06:47 +00:00
|
|
|
def delete_playlist(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
playlist = kwargs.pop("obj")
|
2018-05-08 21:06:47 +00:00
|
|
|
playlist.delete()
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"status": "ok"}
|
2018-05-08 21:06:47 +00:00
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="create_playlist",
|
|
|
|
url_path="createPlaylist",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 21:06:47 +00:00
|
|
|
def create_playlist(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
2018-06-09 13:36:16 +00:00
|
|
|
name = data.get("name", "")
|
2018-05-08 21:06:47 +00:00
|
|
|
if not name:
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"code": 10,
|
|
|
|
"message": "Playlist ID or name must be specified.",
|
|
|
|
}
|
2018-05-10 15:31:49 +00:00
|
|
|
}
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-08 21:06:47 +00:00
|
|
|
|
2018-06-09 13:36:16 +00:00
|
|
|
playlist = request.user.playlists.create(name=name)
|
2018-05-09 18:31:47 +00:00
|
|
|
ids = []
|
2018-06-09 13:36:16 +00:00
|
|
|
for i in data.getlist("songId"):
|
2018-05-09 18:31:47 +00:00
|
|
|
try:
|
|
|
|
ids.append(int(i))
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
if ids:
|
|
|
|
tracks = music_models.Track.objects.filter(pk__in=ids)
|
|
|
|
by_id = {t.id: t for t in tracks}
|
|
|
|
sorted_tracks = []
|
|
|
|
for i in ids:
|
|
|
|
try:
|
|
|
|
sorted_tracks.append(by_id[i])
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
if sorted_tracks:
|
|
|
|
playlist.insert_many(sorted_tracks)
|
2018-06-09 13:36:16 +00:00
|
|
|
playlist = request.user.playlists.with_tracks_count().get(pk=playlist.pk)
|
|
|
|
data = {"playlist": serializers.get_playlist_detail_data(playlist)}
|
2018-05-08 21:06:47 +00:00
|
|
|
return response.Response(data)
|
2018-05-09 18:32:17 +00:00
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_avatar",
|
|
|
|
url_path="getAvatar",
|
|
|
|
)
|
2018-10-21 16:42:59 +00:00
|
|
|
@find_object(
|
|
|
|
queryset=users_models.User.objects.exclude(avatar=None).exclude(avatar=""),
|
|
|
|
model_field="username__iexact",
|
|
|
|
field="username",
|
|
|
|
cast=str,
|
|
|
|
)
|
|
|
|
def get_avatar(self, request, *args, **kwargs):
|
|
|
|
user = kwargs.pop("obj")
|
|
|
|
mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
|
|
|
|
path = music_views.get_file_path(user.avatar)
|
|
|
|
file_header = mapping[settings.REVERSE_PROXY_TYPE]
|
|
|
|
# let the proxy set the content-type
|
|
|
|
r = response.Response({}, content_type="")
|
|
|
|
r[file_header] = path
|
|
|
|
return r
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False, methods=["get", "post"], url_name="get_user", url_path="getUser"
|
|
|
|
)
|
2018-10-21 16:43:25 +00:00
|
|
|
@find_object(
|
|
|
|
queryset=lambda request: users_models.User.objects.filter(pk=request.user.pk),
|
|
|
|
model_field="username__iexact",
|
|
|
|
field="username",
|
|
|
|
cast=str,
|
|
|
|
)
|
|
|
|
def get_user(self, request, *args, **kwargs):
|
|
|
|
data = {"user": serializers.get_user_detail_data(request.user)}
|
|
|
|
return response.Response(data)
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
2018-06-09 13:36:16 +00:00
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_music_folders",
|
|
|
|
url_path="getMusicFolders",
|
|
|
|
)
|
2018-05-09 18:32:17 +00:00
|
|
|
def get_music_folders(self, request, *args, **kwargs):
|
2018-06-09 13:36:16 +00:00
|
|
|
data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}}
|
2018-05-09 18:32:17 +00:00
|
|
|
return response.Response(data)
|
2018-05-31 21:46:15 +00:00
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_cover_art",
|
|
|
|
url_path="getCoverArt",
|
2018-06-09 13:36:16 +00:00
|
|
|
)
|
2018-05-31 21:46:15 +00:00
|
|
|
def get_cover_art(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
2018-06-09 13:36:16 +00:00
|
|
|
id = data.get("id", "")
|
2018-05-31 21:46:15 +00:00
|
|
|
if not id:
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{"error": {"code": 10, "message": "cover art ID must be specified."}}
|
|
|
|
)
|
2018-06-01 21:59:08 +00:00
|
|
|
|
2018-06-09 13:36:16 +00:00
|
|
|
if id.startswith("al-"):
|
2018-05-31 21:46:15 +00:00
|
|
|
try:
|
2018-06-09 13:36:16 +00:00
|
|
|
album_id = int(id.replace("al-", ""))
|
|
|
|
album = (
|
2019-11-25 08:49:06 +00:00
|
|
|
music_models.Album.objects.exclude(attachment_cover=None)
|
|
|
|
.select_related("attachment_cover")
|
2018-06-09 13:36:16 +00:00
|
|
|
.get(pk=album_id)
|
|
|
|
)
|
2018-05-31 21:46:15 +00:00
|
|
|
except (TypeError, ValueError, music_models.Album.DoesNotExist):
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{"error": {"code": 70, "message": "cover art not found."}}
|
|
|
|
)
|
2019-11-25 08:49:06 +00:00
|
|
|
attachment = album.attachment_cover
|
2020-03-18 14:52:23 +00:00
|
|
|
elif id.startswith("at-"):
|
|
|
|
try:
|
|
|
|
attachment_id = id.replace("at-", "")
|
|
|
|
attachment = common_models.Attachment.objects.get(uuid=attachment_id)
|
|
|
|
except (TypeError, ValueError, music_models.Album.DoesNotExist):
|
|
|
|
return response.Response(
|
|
|
|
{"error": {"code": 70, "message": "cover art not found."}}
|
|
|
|
)
|
2018-05-31 21:46:15 +00:00
|
|
|
else:
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{"error": {"code": 70, "message": "cover art not found."}}
|
|
|
|
)
|
2018-05-31 21:46:15 +00:00
|
|
|
|
2019-11-25 08:49:06 +00:00
|
|
|
if not attachment.file:
|
|
|
|
common_tasks.fetch_remote_attachment(attachment)
|
|
|
|
cover = attachment.file
|
2018-06-09 13:36:16 +00:00
|
|
|
mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
|
2018-05-31 21:46:15 +00:00
|
|
|
path = music_views.get_file_path(cover)
|
|
|
|
file_header = mapping[settings.REVERSE_PROXY_TYPE]
|
|
|
|
# let the proxy set the content-type
|
2018-06-09 13:36:16 +00:00
|
|
|
r = response.Response({}, content_type="")
|
2018-05-31 21:46:15 +00:00
|
|
|
r[file_header] = path
|
2018-06-01 21:59:08 +00:00
|
|
|
return r
|
|
|
|
|
2019-01-11 12:33:35 +00:00
|
|
|
@action(
|
|
|
|
detail=False, methods=["get", "post"], url_name="scrobble", url_path="scrobble"
|
|
|
|
)
|
2018-06-01 21:59:08 +00:00
|
|
|
def scrobble(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
serializer = serializers.ScrobbleSerializer(
|
2018-06-09 13:36:16 +00:00
|
|
|
data=data, context={"user": request.user}
|
|
|
|
)
|
2018-06-01 21:59:08 +00:00
|
|
|
if not serializer.is_valid():
|
2018-06-09 13:36:16 +00:00
|
|
|
return response.Response(
|
|
|
|
{"error": {"code": 0, "message": "Invalid payload"}}
|
|
|
|
)
|
|
|
|
if serializer.validated_data["submission"]:
|
2018-06-10 10:06:46 +00:00
|
|
|
listening = serializer.save()
|
|
|
|
record.send(listening)
|
2018-06-01 21:59:08 +00:00
|
|
|
return response.Response({})
|
2019-07-19 06:19:36 +00:00
|
|
|
|
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_genres",
|
|
|
|
url_path="getGenres",
|
|
|
|
)
|
|
|
|
def get_genres(self, request, *args, **kwargs):
|
|
|
|
album_ct = ContentType.objects.get_for_model(music_models.Album)
|
|
|
|
track_ct = ContentType.objects.get_for_model(music_models.Track)
|
|
|
|
queryset = (
|
|
|
|
tags_models.Tag.objects.annotate(
|
|
|
|
_albums_count=Count(
|
|
|
|
"tagged_items", filter=Q(tagged_items__content_type=album_ct)
|
|
|
|
),
|
|
|
|
_tracks_count=Count(
|
|
|
|
"tagged_items", filter=Q(tagged_items__content_type=track_ct)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.exclude(_tracks_count=0, _albums_count=0)
|
|
|
|
.order_by("name")
|
|
|
|
)
|
|
|
|
data = {
|
|
|
|
"genres": {"genre": [serializers.get_genre_data(tag) for tag in queryset]}
|
|
|
|
}
|
|
|
|
return response.Response(data)
|
2020-03-18 14:52:23 +00:00
|
|
|
|
|
|
|
# podcast related views
|
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="create_podcast_channel",
|
|
|
|
url_path="createPodcastChannel",
|
|
|
|
)
|
|
|
|
@requires_channels
|
|
|
|
@transaction.atomic
|
|
|
|
def create_podcast_channel(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
serializer = audio_serializers.RssSubscribeSerializer(data=data)
|
|
|
|
if not serializer.is_valid():
|
|
|
|
return response.Response({"error": {"code": 0, "message": "invalid url"}})
|
|
|
|
channel = (
|
|
|
|
audio_models.Channel.objects.filter(
|
|
|
|
rss_url=serializer.validated_data["url"],
|
|
|
|
)
|
|
|
|
.order_by("id")
|
|
|
|
.first()
|
|
|
|
)
|
|
|
|
if not channel:
|
|
|
|
# try to retrieve the channel via its URL and create it
|
|
|
|
try:
|
|
|
|
channel, uploads = audio_serializers.get_channel_from_rss_url(
|
|
|
|
serializer.validated_data["url"]
|
|
|
|
)
|
|
|
|
except audio_serializers.FeedFetchException as e:
|
|
|
|
return response.Response(
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"code": 0,
|
|
|
|
"message": "Error while fetching url: {}".format(e),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
subscription = federation_models.Follow(actor=request.user.actor)
|
|
|
|
subscription.fid = subscription.get_federation_id()
|
|
|
|
audio_views.SubscriptionsViewSet.queryset.get_or_create(
|
|
|
|
target=channel.actor,
|
|
|
|
actor=request.user.actor,
|
|
|
|
defaults={
|
|
|
|
"approved": True,
|
|
|
|
"fid": subscription.fid,
|
|
|
|
"uuid": subscription.uuid,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
return response.Response({"status": "ok"})
|
|
|
|
|
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="delete_podcast_channel",
|
|
|
|
url_path="deletePodcastChannel",
|
|
|
|
)
|
|
|
|
@requires_channels
|
|
|
|
@find_object(
|
|
|
|
audio_models.Channel.objects.all().select_related("actor"),
|
|
|
|
model_field="uuid",
|
|
|
|
field="id",
|
|
|
|
cast=str,
|
|
|
|
)
|
|
|
|
def delete_podcast_channel(self, request, *args, **kwargs):
|
|
|
|
channel = kwargs.pop("obj")
|
|
|
|
actor = request.user.actor
|
|
|
|
actor.emitted_follows.filter(target=channel.actor).delete()
|
|
|
|
return response.Response({"status": "ok"})
|
|
|
|
|
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_podcasts",
|
|
|
|
url_path="getPodcasts",
|
|
|
|
)
|
|
|
|
@requires_channels
|
|
|
|
def get_podcasts(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
id = data.get("id")
|
|
|
|
channels = audio_models.Channel.objects.subscribed(request.user.actor)
|
|
|
|
if id:
|
|
|
|
channels = channels.filter(uuid=id)
|
|
|
|
channels = channels.select_related(
|
|
|
|
"artist__attachment_cover", "artist__description", "library", "actor"
|
|
|
|
)
|
|
|
|
uploads_qs = (
|
|
|
|
music_models.Upload.objects.playable_by(request.user.actor)
|
|
|
|
.select_related("track__attachment_cover", "track__description",)
|
|
|
|
.order_by("-track__creation_date")
|
|
|
|
)
|
|
|
|
|
|
|
|
if data.get("includeEpisodes", "true") == "true":
|
|
|
|
channels = channels.prefetch_related(
|
|
|
|
Prefetch(
|
|
|
|
"library__uploads",
|
|
|
|
queryset=uploads_qs,
|
|
|
|
to_attr="_prefetched_uploads",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
"podcasts": {
|
|
|
|
"channel": [
|
|
|
|
serializers.get_channel_data(
|
|
|
|
channel, getattr(channel.library, "_prefetched_uploads", [])
|
|
|
|
)
|
|
|
|
for channel in channels
|
|
|
|
]
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return response.Response(data)
|
|
|
|
|
|
|
|
@action(
|
|
|
|
detail=False,
|
|
|
|
methods=["get", "post"],
|
|
|
|
url_name="get_newest_podcasts",
|
|
|
|
url_path="getNewestPodcasts",
|
|
|
|
)
|
|
|
|
@requires_channels
|
|
|
|
def get_newest_podcasts(self, request, *args, **kwargs):
|
|
|
|
data = request.GET or request.POST
|
|
|
|
try:
|
|
|
|
count = int(data["count"])
|
|
|
|
except (TypeError, KeyError, ValueError):
|
|
|
|
count = 20
|
|
|
|
channels = audio_models.Channel.objects.subscribed(request.user.actor)
|
|
|
|
uploads = (
|
|
|
|
music_models.Upload.objects.playable_by(request.user.actor)
|
|
|
|
.filter(library__channel__in=channels)
|
|
|
|
.select_related(
|
|
|
|
"track__attachment_cover", "track__description", "library__channel"
|
|
|
|
)
|
|
|
|
.order_by("-track__creation_date")
|
|
|
|
)
|
|
|
|
data = {
|
|
|
|
"newestPodcasts": {
|
|
|
|
"episode": [
|
|
|
|
serializers.get_channel_episode_data(
|
|
|
|
upload, upload.library.channel.uuid
|
|
|
|
)
|
|
|
|
for upload in uploads[:count]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return response.Response(data)
|