Merge branch '575-subsonic-folders' into 'develop'
Implement more subsonic endpoints See merge request funkwhale/funkwhale!457
This commit is contained in:
commit
c3fece423d
|
@ -135,9 +135,9 @@ class ArtistQuerySet(models.QuerySet):
|
||||||
def playable_by(self, actor, include=True):
|
def playable_by(self, actor, include=True):
|
||||||
tracks = Track.objects.playable_by(actor, include)
|
tracks = Track.objects.playable_by(actor, include)
|
||||||
if include:
|
if include:
|
||||||
return self.filter(tracks__in=tracks)
|
return self.filter(tracks__in=tracks).distinct()
|
||||||
else:
|
else:
|
||||||
return self.exclude(tracks__in=tracks)
|
return self.exclude(tracks__in=tracks).distinct()
|
||||||
|
|
||||||
|
|
||||||
class Artist(APIModelMixin):
|
class Artist(APIModelMixin):
|
||||||
|
@ -203,9 +203,9 @@ class AlbumQuerySet(models.QuerySet):
|
||||||
def playable_by(self, actor, include=True):
|
def playable_by(self, actor, include=True):
|
||||||
tracks = Track.objects.playable_by(actor, include)
|
tracks = Track.objects.playable_by(actor, include)
|
||||||
if include:
|
if include:
|
||||||
return self.filter(tracks__in=tracks)
|
return self.filter(tracks__in=tracks).distinct()
|
||||||
else:
|
else:
|
||||||
return self.exclude(tracks__in=tracks)
|
return self.exclude(tracks__in=tracks).distinct()
|
||||||
|
|
||||||
|
|
||||||
class Album(APIModelMixin):
|
class Album(APIModelMixin):
|
||||||
|
@ -399,9 +399,9 @@ class TrackQuerySet(models.QuerySet):
|
||||||
def playable_by(self, actor, include=True):
|
def playable_by(self, actor, include=True):
|
||||||
files = Upload.objects.playable_by(actor, include)
|
files = Upload.objects.playable_by(actor, include)
|
||||||
if include:
|
if include:
|
||||||
return self.filter(uploads__in=files)
|
return self.filter(uploads__in=files).distinct()
|
||||||
else:
|
else:
|
||||||
return self.exclude(uploads__in=files)
|
return self.exclude(uploads__in=files).distinct()
|
||||||
|
|
||||||
def annotate_duration(self):
|
def annotate_duration(self):
|
||||||
first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk")
|
first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk")
|
||||||
|
@ -557,8 +557,10 @@ class UploadQuerySet(models.QuerySet):
|
||||||
libraries = Library.objects.viewable_by(actor)
|
libraries = Library.objects.viewable_by(actor)
|
||||||
|
|
||||||
if include:
|
if include:
|
||||||
return self.filter(library__in=libraries, import_status="finished")
|
return self.filter(
|
||||||
return self.exclude(library__in=libraries, import_status="finished")
|
library__in=libraries, import_status="finished"
|
||||||
|
).distinct()
|
||||||
|
return self.exclude(library__in=libraries, import_status="finished").distinct()
|
||||||
|
|
||||||
def local(self, include=True):
|
def local(self, include=True):
|
||||||
return self.exclude(library__actor__user__isnull=include)
|
return self.exclude(library__actor__user__isnull=include)
|
||||||
|
|
|
@ -51,9 +51,9 @@ class PlaylistQuerySet(models.QuerySet):
|
||||||
def playable_by(self, actor, include=True):
|
def playable_by(self, actor, include=True):
|
||||||
plts = PlaylistTrack.objects.playable_by(actor, include)
|
plts = PlaylistTrack.objects.playable_by(actor, include)
|
||||||
if include:
|
if include:
|
||||||
return self.filter(playlist_tracks__in=plts)
|
return self.filter(playlist_tracks__in=plts).distinct()
|
||||||
else:
|
else:
|
||||||
return self.exclude(playlist_tracks__in=plts)
|
return self.exclude(playlist_tracks__in=plts).distinct()
|
||||||
|
|
||||||
|
|
||||||
class Playlist(models.Model):
|
class Playlist(models.Model):
|
||||||
|
@ -167,9 +167,9 @@ class PlaylistTrackQuerySet(models.QuerySet):
|
||||||
def playable_by(self, actor, include=True):
|
def playable_by(self, actor, include=True):
|
||||||
tracks = music_models.Track.objects.playable_by(actor, include)
|
tracks = music_models.Track.objects.playable_by(actor, include)
|
||||||
if include:
|
if include:
|
||||||
return self.filter(track__pk__in=tracks)
|
return self.filter(track__pk__in=tracks).distinct()
|
||||||
else:
|
else:
|
||||||
return self.exclude(track__pk__in=tracks)
|
return self.exclude(track__pk__in=tracks).distinct()
|
||||||
|
|
||||||
|
|
||||||
class PlaylistTrack(models.Model):
|
class PlaylistTrack(models.Model):
|
||||||
|
|
|
@ -15,7 +15,7 @@ class SubsonicJSONRenderer(renderers.JSONRenderer):
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
"type": "funkwhale",
|
"type": "funkwhale",
|
||||||
"funkwhale-version": funkwhale_api.__version__,
|
"funkwhaleVersion": funkwhale_api.__version__,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final["subsonic-response"].update(data)
|
final["subsonic-response"].update(data)
|
||||||
|
|
|
@ -226,6 +226,30 @@ def get_music_directory_data(artist):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_folders(user):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_detail_data(user):
|
||||||
|
return {
|
||||||
|
"username": user.username,
|
||||||
|
"email": user.email,
|
||||||
|
"scrobblingEnabled": "true",
|
||||||
|
"adminRole": "false",
|
||||||
|
"settingsRole": "false",
|
||||||
|
"commentRole": "false",
|
||||||
|
"podcastRole": "false",
|
||||||
|
"coverArtRole": "false",
|
||||||
|
"shareRole": "false",
|
||||||
|
"uploadRole": "true",
|
||||||
|
"downloadRole": "true",
|
||||||
|
"playlistRole": "true",
|
||||||
|
"streamRole": "true",
|
||||||
|
"jukeboxRole": "true",
|
||||||
|
"folder": [f["id"] for f in get_folders(user)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleSerializer(serializers.Serializer):
|
class ScrobbleSerializer(serializers.Serializer):
|
||||||
submission = serializers.BooleanField(default=True, required=False)
|
submission = serializers.BooleanField(default=True, required=False)
|
||||||
id = serializers.PrimaryKeyRelatedField(
|
id = serializers.PrimaryKeyRelatedField(
|
||||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework import renderers, response, viewsets
|
||||||
from rest_framework.decorators import list_route
|
from rest_framework.decorators import list_route
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
|
import funkwhale_api
|
||||||
from funkwhale_api.activity import record
|
from funkwhale_api.activity import record
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.favorites.models import TrackFavorite
|
from funkwhale_api.favorites.models import TrackFavorite
|
||||||
|
@ -15,6 +16,7 @@ from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.music import utils
|
from funkwhale_api.music import utils
|
||||||
from funkwhale_api.music import views as music_views
|
from funkwhale_api.music import views as music_views
|
||||||
from funkwhale_api.playlists import models as playlists_models
|
from funkwhale_api.playlists import models as playlists_models
|
||||||
|
from funkwhale_api.users import models as users_models
|
||||||
|
|
||||||
from . import authentication, filters, negotiation, serializers
|
from . import authentication, filters, negotiation, serializers
|
||||||
|
|
||||||
|
@ -121,6 +123,8 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
data = {
|
data = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
|
"type": "funkwhale",
|
||||||
|
"funkwhaleVersion": funkwhale_api.__version__,
|
||||||
"license": {
|
"license": {
|
||||||
"valid": "true",
|
"valid": "true",
|
||||||
"email": "valid@valid.license",
|
"email": "valid@valid.license",
|
||||||
|
@ -218,6 +222,29 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
|
data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
|
||||||
return response.Response(data)
|
return response.Response(data)
|
||||||
|
|
||||||
|
@list_route(
|
||||||
|
methods=["get", "post"], url_name="get_random_songs", url_path="getRandomSongs"
|
||||||
|
)
|
||||||
|
def get_random_songs(self, request, *args, **kwargs):
|
||||||
|
data = request.GET or request.POST
|
||||||
|
actor = utils.get_actor_from_request(request)
|
||||||
|
queryset = music_models.Track.objects.all()
|
||||||
|
queryset = queryset.playable_by(actor)
|
||||||
|
try:
|
||||||
|
size = int(data["size"])
|
||||||
|
except (TypeError, KeyError, ValueError):
|
||||||
|
size = 50
|
||||||
|
|
||||||
|
queryset = (
|
||||||
|
queryset.playable_by(actor).prefetch_related("uploads").order_by("?")[:size]
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"randomSongs": {
|
||||||
|
"song": serializers.GetSongSerializer(queryset, many=True).data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.Response(data)
|
||||||
|
|
||||||
@list_route(methods=["get", "post"], url_name="get_starred", url_path="getStarred")
|
@list_route(methods=["get", "post"], url_name="get_starred", url_path="getStarred")
|
||||||
def get_starred(self, request, *args, **kwargs):
|
def get_starred(self, request, *args, **kwargs):
|
||||||
favorites = request.user.track_favorites.all()
|
favorites = request.user.track_favorites.all()
|
||||||
|
@ -426,6 +453,34 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
data = {"playlist": serializers.get_playlist_detail_data(playlist)}
|
data = {"playlist": serializers.get_playlist_detail_data(playlist)}
|
||||||
return response.Response(data)
|
return response.Response(data)
|
||||||
|
|
||||||
|
@list_route(methods=["get", "post"], url_name="get_avatar", url_path="getAvatar")
|
||||||
|
@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
|
||||||
|
|
||||||
|
@list_route(methods=["get", "post"], url_name="get_user", url_path="getUser")
|
||||||
|
@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)
|
||||||
|
|
||||||
@list_route(
|
@list_route(
|
||||||
methods=["get", "post"],
|
methods=["get", "post"],
|
||||||
url_name="get_music_folders",
|
url_name="get_music_folders",
|
||||||
|
|
|
@ -13,7 +13,7 @@ def test_json_renderer():
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
"type": "funkwhale",
|
"type": "funkwhale",
|
||||||
"funkwhale-version": funkwhale_api.__version__,
|
"funkwhaleVersion": funkwhale_api.__version__,
|
||||||
"hello": "world",
|
"hello": "world",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ def test_render_content_json(db, api_client):
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
"type": "funkwhale",
|
"type": "funkwhale",
|
||||||
"funkwhale-version": funkwhale_api.__version__,
|
"funkwhaleVersion": funkwhale_api.__version__,
|
||||||
}
|
}
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.content) == render_json(expected)
|
assert json.loads(response.content) == render_json(expected)
|
||||||
|
@ -60,6 +60,8 @@ def test_get_license(f, db, logged_in_api_client, mocker):
|
||||||
expected = {
|
expected = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
|
"type": "funkwhale",
|
||||||
|
"funkwhaleVersion": funkwhale_api.__version__,
|
||||||
"license": {
|
"license": {
|
||||||
"valid": "true",
|
"valid": "true",
|
||||||
"email": "valid@valid.license",
|
"email": "valid@valid.license",
|
||||||
|
@ -245,6 +247,29 @@ def test_get_starred2(f, db, logged_in_api_client, factories):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
|
def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
|
||||||
|
url = reverse("api:subsonic-get-random-songs")
|
||||||
|
assert url.endswith("getRandomSongs") is True
|
||||||
|
track1 = factories["music.Track"]()
|
||||||
|
track2 = factories["music.Track"]()
|
||||||
|
factories["music.Track"]()
|
||||||
|
|
||||||
|
order_by = mocker.patch.object(
|
||||||
|
music_models.TrackQuerySet, "order_by", return_value=[track1, track2]
|
||||||
|
)
|
||||||
|
response = logged_in_api_client.get(url, {"f": f, "size": 2})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"randomSongs": {
|
||||||
|
"song": serializers.GetSongSerializer([track1, track2], many=True).data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
order_by.assert_called_once_with("?")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("f", ["xml", "json"])
|
@pytest.mark.parametrize("f", ["xml", "json"])
|
||||||
def test_get_starred(f, db, logged_in_api_client, factories):
|
def test_get_starred(f, db, logged_in_api_client, factories):
|
||||||
url = reverse("api:subsonic-get-starred")
|
url = reverse("api:subsonic-get-starred")
|
||||||
|
@ -457,6 +482,19 @@ def test_get_cover_art_album(factories, logged_in_api_client):
|
||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_avatar(factories, logged_in_api_client):
|
||||||
|
user = factories["users.User"]()
|
||||||
|
url = reverse("api:subsonic-get-avatar")
|
||||||
|
assert url.endswith("getAvatar") is True
|
||||||
|
response = logged_in_api_client.get(url, {"username": user.username})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response["Content-Type"] == ""
|
||||||
|
assert response["X-Accel-Redirect"] == music_views.get_file_path(
|
||||||
|
user.avatar
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_scrobble(factories, logged_in_api_client):
|
def test_scrobble(factories, logged_in_api_client):
|
||||||
upload = factories["music.Upload"]()
|
upload = factories["music.Upload"]()
|
||||||
track = upload.track
|
track = upload.track
|
||||||
|
@ -468,3 +506,34 @@ def test_scrobble(factories, logged_in_api_client):
|
||||||
|
|
||||||
listening = logged_in_api_client.user.listenings.latest("id")
|
listening = logged_in_api_client.user.listenings.latest("id")
|
||||||
assert listening.track == track
|
assert listening.track == track
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
|
def test_get_user(f, db, logged_in_api_client, factories):
|
||||||
|
url = reverse("api:subsonic-get-user")
|
||||||
|
assert url.endswith("getUser") is True
|
||||||
|
response = logged_in_api_client.get(
|
||||||
|
url, {"f": f, "username": logged_in_api_client.user.username}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"user": {
|
||||||
|
"username": logged_in_api_client.user.username,
|
||||||
|
"email": logged_in_api_client.user.email,
|
||||||
|
"scrobblingEnabled": "true",
|
||||||
|
"adminRole": "false",
|
||||||
|
"downloadRole": "true",
|
||||||
|
"uploadRole": "true",
|
||||||
|
"settingsRole": "false",
|
||||||
|
"playlistRole": "true",
|
||||||
|
"commentRole": "false",
|
||||||
|
"podcastRole": "false",
|
||||||
|
"streamRole": "true",
|
||||||
|
"jukeboxRole": "true",
|
||||||
|
"coverArtRole": "false",
|
||||||
|
"shareRole": "false",
|
||||||
|
"folder": [
|
||||||
|
f["id"] for f in serializers.get_folders(logged_in_api_client.user)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed missing track count on various library cards (#581)
|
Loading…
Reference in New Issue