diff --git a/api/funkwhale_api/subsonic/renderers.py b/api/funkwhale_api/subsonic/renderers.py index f11940222..e9703d48d 100644 --- a/api/funkwhale_api/subsonic/renderers.py +++ b/api/funkwhale_api/subsonic/renderers.py @@ -32,9 +32,11 @@ ET._serialize_xml = ET._serialize["xml"] = _serialize_xml def structure_payload(data): payload = { "funkwhaleVersion": funkwhale_api.__version__, + "serverVersion": funkwhale_api.__version__, "status": "ok", "type": "funkwhale", "version": "1.16.0", + "openSubsonic": "true", } payload.update(data) if "detail" in payload: diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index 5a615126b..12255d622 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -126,6 +126,7 @@ def get_track_data(album, track, upload): "albumId": album.pk if album else "", "artistId": album.artist.pk if album else track.artist.pk, "type": "music", + "musicBrainzId": str(track.mbid or ""), } if album and album.attachment_cover_id: data["coverArt"] = f"al-{album.id}" @@ -149,13 +150,16 @@ def get_album2_data(album): "created": to_subsonic_date(album.creation_date), "duration": album.duration, "playCount": album.tracks.aggregate(l=Count("listenings"))["l"] or 0, + "musicBrainzId": str(album.mbid or ""), } if album.attachment_cover_id: payload["coverArt"] = f"al-{album.id}" if album.tagged_items: + genres = [{"name": i.tag.name} for i in album.tagged_items.all()] # exposes only first genre since the specification uses singular noun - first_genre = album.tagged_items.first() - payload["genre"] = first_genre.tag.name if first_genre else "" + payload["genre"] = genres[0]["name"] if len(genres) > 0 else "" + # OpenSubsonic full genre list + payload["genres"] = genres if album.release_date: payload["year"] = album.release_date.year try: diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 316cce706..c859ed7de 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -180,6 +180,20 @@ class SubsonicViewSet(viewsets.GenericViewSet): } return response.Response(data, status=200) + @action( + detail=False, + methods=["get", "post"], + url_name="get_open_subsonic_extensions", + permission_classes=[], + url_path="getOpenSubsonicExtensions", + ) + def get_open_subsonic_extensions(self, request, *args, **kwargs): + data = { + # No specific extensions are currently supported + "openSubsonicExtensions": [""], + } + return response.Response(data, status=200) + @action( detail=False, methods=["get", "post"], diff --git a/api/tests/subsonic/test_renderers.py b/api/tests/subsonic/test_renderers.py index 551f5572c..1b49753e4 100644 --- a/api/tests/subsonic/test_renderers.py +++ b/api/tests/subsonic/test_renderers.py @@ -17,6 +17,8 @@ from funkwhale_api.subsonic import renderers "version": "1.16.0", "type": "funkwhale", "funkwhaleVersion": funkwhale_api.__version__, + "serverVersion": funkwhale_api.__version__, + "openSubsonic": "true", "hello": "world", }, ), @@ -30,6 +32,8 @@ from funkwhale_api.subsonic import renderers "version": "1.16.0", "type": "funkwhale", "funkwhaleVersion": funkwhale_api.__version__, + "serverVersion": funkwhale_api.__version__, + "openSubsonic": "true", "hello": "world", "error": {"code": 10, "message": "something went wrong"}, }, @@ -41,6 +45,8 @@ from funkwhale_api.subsonic import renderers "version": "1.16.0", "type": "funkwhale", "funkwhaleVersion": funkwhale_api.__version__, + "serverVersion": funkwhale_api.__version__, + "openSubsonic": "true", "hello": "world", "error": {"code": 0, "message": "something went wrong"}, }, @@ -59,6 +65,8 @@ def test_json_renderer(): "version": "1.16.0", "type": "funkwhale", "funkwhaleVersion": funkwhale_api.__version__, + "serverVersion": funkwhale_api.__version__, + "openSubsonic": "true", "hello": "world", } } @@ -81,8 +89,9 @@ def test_xml_renderer_dict_to_xml(): def test_xml_renderer(): payload = {"hello": "world"} - expected = '\n' # noqa - expected = expected.format(funkwhale_api.__version__).encode() + expected = '\n' # noqa + version = funkwhale_api.__version__ + expected = expected.format(version, version).encode() renderer = renderers.SubsonicXMLRenderer() rendered = renderer.render(payload) diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index bb419e61d..7c54bddd7 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -184,6 +184,8 @@ def test_get_album_serializer(factories): "year": album.release_date.year, "coverArt": f"al-{album.id}", "genre": tagged_item.tag.name, + "genres": [{"name": tagged_item.tag.name}], + "musicBrainzId": album.mbid, "duration": 43, "playCount": album.tracks.aggregate(l=Count("listenings"))["l"] or 0, "song": [ @@ -207,6 +209,7 @@ def test_get_album_serializer(factories): "albumId": album.pk, "artistId": artist.pk, "type": "music", + "musicBrainzId": track.mbid, } ], } diff --git a/changes/changelog.d/opensubsonic.feature b/changes/changelog.d/opensubsonic.feature new file mode 100644 index 000000000..8d28a02cb --- /dev/null +++ b/changes/changelog.d/opensubsonic.feature @@ -0,0 +1 @@ +Extend Subsonic API with OpenSubsonic support (#2270)