From d3e411fa9720c6c2892b29eda06b6025e3597480 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 18:42:59 +0200 Subject: [PATCH 1/8] See #574: Implemented getAvatar view --- api/funkwhale_api/subsonic/views.py | 18 ++++++++++++++++++ api/tests/subsonic/test_views.py | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 7ca9b13a8..2014c69da 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -15,6 +15,7 @@ from funkwhale_api.music import models as music_models from funkwhale_api.music import utils from funkwhale_api.music import views as music_views from funkwhale_api.playlists import models as playlists_models +from funkwhale_api.users import models as users_models from . import authentication, filters, negotiation, serializers @@ -426,6 +427,23 @@ class SubsonicViewSet(viewsets.GenericViewSet): data = {"playlist": serializers.get_playlist_detail_data(playlist)} 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_music_folders", diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index f856146ec..82918b631 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -457,6 +457,19 @@ def test_get_cover_art_album(factories, logged_in_api_client): ).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): upload = factories["music.Upload"]() track = upload.track From 439d4fd6b33cf188d0d13ec1f4d8bec75cf5a0b2 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 18:43:25 +0200 Subject: [PATCH 2/8] See #574: Implemented getUser view --- api/funkwhale_api/subsonic/serializers.py | 24 ++++++++++++++++++ api/funkwhale_api/subsonic/views.py | 11 ++++++++ api/tests/subsonic/test_views.py | 31 +++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index 35b178641..023e40cac 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -226,6 +226,30 @@ def get_music_directory_data(artist): 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): submission = serializers.BooleanField(default=True, required=False) id = serializers.PrimaryKeyRelatedField( diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 2014c69da..8c4f26dde 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -444,6 +444,17 @@ class SubsonicViewSet(viewsets.GenericViewSet): 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( methods=["get", "post"], url_name="get_music_folders", diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 82918b631..9161888e2 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -481,3 +481,34 @@ def test_scrobble(factories, logged_in_api_client): listening = logged_in_api_client.user.listenings.latest("id") 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) + ], + } + } From 8c5924d6e3c4bd66d9f013aa2b5e8d7ec7e8ec97 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 19:58:05 +0200 Subject: [PATCH 3/8] Ensure distinct is applied on all playable_by querysets --- api/funkwhale_api/music/models.py | 16 ++++++++-------- api/funkwhale_api/playlists/models.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 318640c88..6d3c0dffd 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -135,9 +135,9 @@ class ArtistQuerySet(models.QuerySet): def playable_by(self, actor, include=True): tracks = Track.objects.playable_by(actor, include) if include: - return self.filter(tracks__in=tracks) + return self.filter(tracks__in=tracks).distinct() else: - return self.exclude(tracks__in=tracks) + return self.exclude(tracks__in=tracks).distinct() class Artist(APIModelMixin): @@ -203,9 +203,9 @@ class AlbumQuerySet(models.QuerySet): def playable_by(self, actor, include=True): tracks = Track.objects.playable_by(actor, include) if include: - return self.filter(tracks__in=tracks) + return self.filter(tracks__in=tracks).distinct() else: - return self.exclude(tracks__in=tracks) + return self.exclude(tracks__in=tracks).distinct() class Album(APIModelMixin): @@ -399,9 +399,9 @@ class TrackQuerySet(models.QuerySet): def playable_by(self, actor, include=True): files = Upload.objects.playable_by(actor, include) if include: - return self.filter(uploads__in=files) + return self.filter(uploads__in=files).distinct() else: - return self.exclude(uploads__in=files) + return self.exclude(uploads__in=files).distinct() def annotate_duration(self): first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk") @@ -557,8 +557,8 @@ class UploadQuerySet(models.QuerySet): libraries = Library.objects.viewable_by(actor) if include: - return self.filter(library__in=libraries, import_status="finished") - return self.exclude(library__in=libraries, import_status="finished") + return self.filter(library__in=libraries, import_status="finished").distinct() + return self.exclude(library__in=libraries, import_status="finished").distinct() def local(self, include=True): return self.exclude(library__actor__user__isnull=include) diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py index ac86b97a0..e1895137d 100644 --- a/api/funkwhale_api/playlists/models.py +++ b/api/funkwhale_api/playlists/models.py @@ -51,9 +51,9 @@ class PlaylistQuerySet(models.QuerySet): def playable_by(self, actor, include=True): plts = PlaylistTrack.objects.playable_by(actor, include) if include: - return self.filter(playlist_tracks__in=plts) + return self.filter(playlist_tracks__in=plts).distinct() else: - return self.exclude(playlist_tracks__in=plts) + return self.exclude(playlist_tracks__in=plts).distinct() class Playlist(models.Model): @@ -167,9 +167,9 @@ class PlaylistTrackQuerySet(models.QuerySet): def playable_by(self, actor, include=True): tracks = music_models.Track.objects.playable_by(actor, include) if include: - return self.filter(track__pk__in=tracks) + return self.filter(track__pk__in=tracks).distinct() else: - return self.exclude(track__pk__in=tracks) + return self.exclude(track__pk__in=tracks).distinct() class PlaylistTrack(models.Model): From 82f2f606c1b4c4dfd44f54e42164fe333cf359d1 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 20:01:12 +0200 Subject: [PATCH 4/8] Missing changelog for #581 --- changes/changelog.d/581.bugfix | 1 + 1 file changed, 1 insertion(+) diff --git a/changes/changelog.d/581.bugfix b/changes/changelog.d/581.bugfix index e69de29bb..9cb1cd42f 100644 --- a/changes/changelog.d/581.bugfix +++ b/changes/changelog.d/581.bugfix @@ -0,0 +1 @@ +Fixed missing track count on various library cards (#581) From ccd600eccaf98aa915f51a07540c1e7dd6ce757f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 20:22:31 +0200 Subject: [PATCH 5/8] See #574: Implemented getRandomSongs endpoint --- api/funkwhale_api/subsonic/views.py | 21 +++++++++++++++++++++ api/tests/subsonic/test_views.py | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 8c4f26dde..37a06f0c7 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -219,6 +219,27 @@ class SubsonicViewSet(viewsets.GenericViewSet): data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}} 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") def get_starred(self, request, *args, **kwargs): favorites = request.user.track_favorites.all() diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 9161888e2..9e71fca94 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -245,6 +245,27 @@ 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"]) def test_get_starred(f, db, logged_in_api_client, factories): url = reverse("api:subsonic-get-starred") From bf3b3854dce7b227a76352799edef6283c8cf4e7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 20:22:50 +0200 Subject: [PATCH 6/8] See #573: Added missing type / version in getLicense endpoint --- api/funkwhale_api/subsonic/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 37a06f0c7..308ed4913 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -8,6 +8,7 @@ from rest_framework import renderers, response, viewsets from rest_framework.decorators import list_route from rest_framework.serializers import ValidationError +import funkwhale_api from funkwhale_api.activity import record from funkwhale_api.common import preferences from funkwhale_api.favorites.models import TrackFavorite @@ -122,6 +123,8 @@ class SubsonicViewSet(viewsets.GenericViewSet): data = { "status": "ok", "version": "1.16.0", + "type": "funkwhale", + "funkwhale-version": funkwhale_api.__version__, "license": { "valid": "true", "email": "valid@valid.license", From 77973ba6f95a605bd57ee2b55cc20db8db653ca8 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 20:33:41 +0200 Subject: [PATCH 7/8] See #574: linting --- api/funkwhale_api/music/models.py | 4 +++- api/funkwhale_api/subsonic/views.py | 4 +++- api/tests/subsonic/test_views.py | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 6d3c0dffd..87f7ba819 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -557,7 +557,9 @@ class UploadQuerySet(models.QuerySet): libraries = Library.objects.viewable_by(actor) if include: - return self.filter(library__in=libraries, import_status="finished").distinct() + return self.filter( + library__in=libraries, import_status="finished" + ).distinct() return self.exclude(library__in=libraries, import_status="finished").distinct() def local(self, include=True): diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 308ed4913..da646fcad 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -235,7 +235,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): except (TypeError, KeyError, ValueError): size = 50 - queryset = queryset.playable_by(actor).prefetch_related('uploads').order_by("?")[:size] + queryset = ( + queryset.playable_by(actor).prefetch_related("uploads").order_by("?")[:size] + ) data = { "randomSongs": { "song": serializers.GetSongSerializer(queryset, many=True).data diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 9e71fca94..8d84758dc 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -254,13 +254,15 @@ def test_get_random_songs(f, db, logged_in_api_client, factories, mocker): factories["music.Track"]() order_by = mocker.patch.object( - music_models.TrackQuerySet, 'order_by', return_value=[track1, track2] + 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} + "randomSongs": { + "song": serializers.GetSongSerializer([track1, track2], many=True).data + } } order_by.assert_called_once_with("?") From aaaa2479dc1e10db74da6120d89fca0b3477a76a Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 21 Oct 2018 20:43:12 +0200 Subject: [PATCH 8/8] Missing changelog and getLicense endpoint for #581 --- api/funkwhale_api/subsonic/renderers.py | 2 +- api/funkwhale_api/subsonic/views.py | 2 +- api/tests/subsonic/test_renderers.py | 2 +- api/tests/subsonic/test_views.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/funkwhale_api/subsonic/renderers.py b/api/funkwhale_api/subsonic/renderers.py index fd98633aa..95b437a55 100644 --- a/api/funkwhale_api/subsonic/renderers.py +++ b/api/funkwhale_api/subsonic/renderers.py @@ -15,7 +15,7 @@ class SubsonicJSONRenderer(renderers.JSONRenderer): "status": "ok", "version": "1.16.0", "type": "funkwhale", - "funkwhale-version": funkwhale_api.__version__, + "funkwhaleVersion": funkwhale_api.__version__, } } final["subsonic-response"].update(data) diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index da646fcad..b8cf4b4bc 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -124,7 +124,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): "status": "ok", "version": "1.16.0", "type": "funkwhale", - "funkwhale-version": funkwhale_api.__version__, + "funkwhaleVersion": funkwhale_api.__version__, "license": { "valid": "true", "email": "valid@valid.license", diff --git a/api/tests/subsonic/test_renderers.py b/api/tests/subsonic/test_renderers.py index e234a993a..7e977ac45 100644 --- a/api/tests/subsonic/test_renderers.py +++ b/api/tests/subsonic/test_renderers.py @@ -13,7 +13,7 @@ def test_json_renderer(): "status": "ok", "version": "1.16.0", "type": "funkwhale", - "funkwhale-version": funkwhale_api.__version__, + "funkwhaleVersion": funkwhale_api.__version__, "hello": "world", } } diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 8d84758dc..7cf5e8f12 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -24,7 +24,7 @@ def test_render_content_json(db, api_client): "status": "ok", "version": "1.16.0", "type": "funkwhale", - "funkwhale-version": funkwhale_api.__version__, + "funkwhaleVersion": funkwhale_api.__version__, } assert response.status_code == 200 assert json.loads(response.content) == render_json(expected) @@ -60,6 +60,8 @@ def test_get_license(f, db, logged_in_api_client, mocker): expected = { "status": "ok", "version": "1.16.0", + "type": "funkwhale", + "funkwhaleVersion": funkwhale_api.__version__, "license": { "valid": "true", "email": "valid@valid.license",