Newer
Older
import datetime
Eliot Berriot
committed
import json
Eliot Berriot
committed
from django.urls import reverse
from rest_framework.response import Response
Eliot Berriot
committed
import funkwhale_api
from funkwhale_api.moderation import filters as moderation_filters
Eliot Berriot
committed
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
from funkwhale_api.subsonic import renderers, serializers
Eliot Berriot
committed
def render_json(data):
return json.loads(renderers.SubsonicJSONRenderer().render(data))
def test_render_content_json(db, api_client):
url = reverse("api:subsonic-ping")
response = api_client.get(url, {"f": "json"})
Eliot Berriot
committed
Eliot Berriot
committed
expected = {
"status": "ok",
"version": "1.16.0",
"type": "funkwhale",
"funkwhaleVersion": funkwhale_api.__version__,
Eliot Berriot
committed
}
Eliot Berriot
committed
assert response.status_code == 200
assert json.loads(response.content) == render_json(expected)
Eliot Berriot
committed
def test_exception_wrong_credentials(f, db, api_client):
url = reverse("api:subsonic-ping")
response = api_client.get(url, {"f": f, "u": "yolo"})
Eliot Berriot
committed
expected = {
"status": "failed",
"error": {"code": 40, "message": "Wrong username or password."},
Eliot Berriot
committed
}
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("f", ["json"])
def test_exception_missing_credentials(f, db, api_client):
response = api_client.get(url)
expected = {
"status": "failed",
"error": {"code": 10, "message": "Required parameter is missing."},
}
assert response.status_code == 200
assert response.data == expected
Eliot Berriot
committed
def test_disabled_subsonic(preferences, api_client):
preferences["subsonic__enabled"] = False
url = reverse("api:subsonic-ping")
Eliot Berriot
committed
response = api_client.get(url)
assert response.status_code == 405
def test_get_license(f, db, logged_in_api_client, mocker):
now = timezone.now()
mocker.patch("django.utils.timezone.now", return_value=now)
response = logged_in_api_client.get(url, {"f": f})
expected = {
"type": "funkwhale",
"funkwhaleVersion": funkwhale_api.__version__,
"license": {
"valid": "true",
"email": "valid@valid.license",
"licenseExpires": now + datetime.timedelta(days=365),
},
}
assert response.status_code == 200
assert response.data == expected
Eliot Berriot
committed
def test_ping(f, db, api_client):
url = reverse("api:subsonic-ping")
response = api_client.get(url, {"f": f})
Eliot Berriot
committed
Eliot Berriot
committed
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("f", ["json"])
def test_get_artists(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
factories["moderation.UserFilter"](
user=logged_in_api_client.user,
target_artist=factories["music.Artist"](playable=True),
)
factories["music.Artist"].create_batch(size=3, playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
exclude_query = moderation_filters.get_filtered_content_query(
moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
)
assert exclude_query is not None
Eliot Berriot
committed
expected = {
music_models.Artist.objects.all().exclude(exclude_query)
Eliot Berriot
committed
).data
}
Eliot Berriot
committed
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(
music_models.Artist.objects.all().exclude(exclude_query),
logged_in_api_client.user.actor,
)
Eliot Berriot
committed
@pytest.mark.parametrize("f", ["json"])
def test_get_artist(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
artist = factories["music.Artist"](playable=True)
factories["music.Album"].create_batch(size=3, artist=artist, playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = {"artist": serializers.GetArtistSerializer(artist).data}
response = logged_in_api_client.get(url, {"id": artist.pk})
Eliot Berriot
committed
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
Eliot Berriot
committed
@pytest.mark.parametrize("f", ["json"])
def test_get_invalid_artist(f, db, logged_in_api_client, factories):
assert url.endswith("getArtist") is True
expected = {"error": {"code": 0, "message": 'For input string "asdf"'}}
response = logged_in_api_client.get(url, {"id": "asdf"})
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("f", ["json"])
def test_get_artist_info2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_artist_info2")
artist = factories["music.Artist"](playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = {"artist-info2": {}}
response = logged_in_api_client.get(url, {"id": artist.pk})
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
@pytest.mark.parametrize("f", ["json"])
def test_get_album(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
assert url.endswith("getAlbum") is True
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist)
factories["music.Track"].create_batch(size=3, album=album, playable=True)
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
expected = {"album": serializers.GetAlbumSerializer(album).data}
response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
Eliot Berriot
committed
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(
music_models.Album.objects.select_related("artist"), None
)
Eliot Berriot
committed
@pytest.mark.parametrize("f", ["json"])
def test_get_song(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
assert url.endswith("getSong") is True
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist)
track = factories["music.Track"](album=album, playable=True)
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200
assert response.data == {
"song": serializers.get_track_data(track.album, track, upload)
}
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@pytest.mark.parametrize("f", ["json"])
def test_stream(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
):
# Even with this settings set to false, we proxy media in the subsonic API
# Because clients don't expect a 302 redirect
settings.PROXY_MEDIA = False
url = reverse("api:subsonic-stream")
mocked_serve = mocker.spy(music_views, "handle_serve")
assert url.endswith("stream") is True
upload = factories["music.Upload"](playable=True)
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
response = logged_in_api_client.get(url, {"f": f, "id": upload.track.pk})
mocked_serve.assert_called_once_with(
upload=upload,
user=logged_in_api_client.user,
format=None,
max_bitrate=None,
proxy_media=True,
wsgi_request=response.wsgi_request,
Eliot Berriot
committed
assert response.status_code == 200
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@pytest.mark.parametrize("format,expected", [("mp3", "mp3"), ("raw", None)])
def test_stream_format(format, expected, logged_in_api_client, factories, mocker):
url = reverse("api:subsonic-stream")
mocked_serve = mocker.patch.object(
music_views, "handle_serve", return_value=Response()
)
upload = factories["music.Upload"](playable=True)
response = logged_in_api_client.get(url, {"id": upload.track.pk, "format": format})
upload=upload,
user=logged_in_api_client.user,
format=expected,
max_bitrate=None,
proxy_media=True,
wsgi_request=response.wsgi_request,
)
assert response.status_code == 200
@pytest.mark.parametrize(
"max_bitrate,format,default_transcoding_format,expected_bitrate,expected_format",
[
# no max bitrate, no format, so no transcoding should happen
(0, "", "ogg", None, None),
# same using "raw" format
(0, "raw", "ogg", None, None),
# specified bitrate, but no format, so fallback to default transcoding format
(192, "", "ogg", 192000, "ogg"),
# specified bitrate, but over limit
(2000, "", "ogg", 320000, "ogg"),
# specified format, we use that one
(192, "opus", "ogg", 192000, "opus"),
# No default transcoding format set and no format requested
(192, "", None, 192000, None),
],
def test_stream_transcode(
max_bitrate,
format,
default_transcoding_format,
expected_bitrate,
expected_format,
logged_in_api_client,
factories,
mocker,
settings,
):
upload = factories["music.Upload"](playable=True)
params = {"id": upload.track.pk, "maxBitRate": max_bitrate}
if format:
params["format"] = format
settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
url = reverse("api:subsonic-stream")
mocked_serve = mocker.patch.object(
music_views, "handle_serve", return_value=Response()
)
response = logged_in_api_client.get(url, params)
mocked_serve.assert_called_once_with(
upload=upload,
user=logged_in_api_client.user,
format=expected_format,
max_bitrate=expected_bitrate,
wsgi_request=response.wsgi_request,
assert response.status_code == 200
@pytest.mark.parametrize("f", ["json"])
def test_star(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-star")
assert url.endswith("star") is True
track = factories["music.Track"]()
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200
favorite = logged_in_api_client.user.track_favorites.latest("id")
assert favorite.track == track
@pytest.mark.parametrize("f", ["json"])
def test_unstar(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-unstar")
assert url.endswith("unstar") is True
track = factories["music.Track"]()
factories["favorites.TrackFavorite"](track=track, user=logged_in_api_client.user)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200
assert logged_in_api_client.user.track_favorites.count() == 0
@pytest.mark.parametrize("f", ["json"])
def test_get_starred2(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_starred2")
assert url.endswith("getStarred2") is True
track = factories["music.Track"]()
favorite = factories["favorites.TrackFavorite"](
track=track, user=logged_in_api_client.user
)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200
assert response.data == {
"starred2": {"song": serializers.get_starred_tracks_data([favorite])}
@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", ["json"])
def test_get_genres(f, db, logged_in_api_client, factories, mocker):
url = reverse("api:subsonic-get_genres")
assert url.endswith("getGenres") is True
tag1 = factories["tags.Tag"](name="Pop")
tag2 = factories["tags.Tag"](name="Rock")
factories["music.Album"](set_tags=[tag1.name, tag2.name])
factories["music.Track"](set_tags=[tag1.name])
response = logged_in_api_client.get(url)
assert response.status_code == 200
assert response.data == {
"genres": {
"genre": [
{"songCount": 1, "albumCount": 1, "value": tag1.name},
{"songCount": 0, "albumCount": 1, "value": tag2.name},
]
}
}
@pytest.mark.parametrize("f", ["json"])
def test_get_starred(f, db, logged_in_api_client, factories):
assert url.endswith("getStarred") is True
track = factories["music.Track"]()
favorite = factories["favorites.TrackFavorite"](
track=track, user=logged_in_api_client.user
)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200
assert response.data == {
"starred": {"song": serializers.get_starred_tracks_data([favorite])}
@pytest.mark.parametrize("f", ["json"])
def test_get_album_list2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_album_list2")
album1 = factories["music.Album"](playable=True)
album2 = factories["music.Album"](playable=True)
factories["music.Album"]()
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
response = logged_in_api_client.get(url, {"f": f, "type": "newest"})
assert response.status_code == 200
assert response.data == {
"albumList2": {"album": serializers.get_album_list2_data([album2, album1])}
playable_by.assert_called_once()
def test_get_album_list2_recent(db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
factories["music.Album"](playable=True, release_date=None)
album2 = factories["music.Album"](playable=True)
album3 = factories["music.Album"](playable=True)
response = logged_in_api_client.get(url, {"f": "json", "type": "recent"})
assert response.status_code == 200
expected_albums = reversed(sorted([album3, album2], key=lambda a: a.release_date))
assert response.data == {
"albumList2": {"album": serializers.get_album_list2_data(expected_albums)}
}
@pytest.mark.parametrize("f", ["json"])
def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"](playable=True)
factories["music.Album"](playable=True)
response = logged_in_api_client.get(
url, {"f": f, "type": "newest", "size": 1, "offset": 1}
)
assert response.status_code == 200
assert response.data == {
"albumList2": {"album": serializers.get_album_list2_data([album1])}
}
@pytest.mark.parametrize("f", ["json"])
def test_get_album_list2_by_genre(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"](
artist__name="Artist1", playable=True, set_tags=["Rock"]
)
album2 = factories["music.Album"](
artist__name="Artist2", playable=True, artist__set_tags=["Rock"]
)
factories["music.Album"](playable=True, set_tags=["Pop"])
response = logged_in_api_client.get(
url, {"f": f, "type": "byGenre", "size": 5, "offset": 0, "genre": "rock"}
)
assert response.status_code == 200
assert response.data == {
"albumList2": {"album": serializers.get_album_list2_data([album1, album2])}
}
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
@pytest.mark.parametrize(
"params, expected",
[
({"type": "byYear", "fromYear": 1902, "toYear": 1903}, [2, 3]),
# Because why not, it's supported in Subsonic API…
# http://www.subsonic.org/pages/api.jsp#getAlbumList2
({"type": "byYear", "fromYear": 1903, "toYear": 1902}, [3, 2]),
],
)
def test_get_album_list2_by_year(params, expected, db, logged_in_api_client, factories):
albums = [
factories["music.Album"](
playable=True, release_date=datetime.date(1900 + i, 1, 1)
)
for i in range(5)
]
url = reverse("api:subsonic-get_album_list2")
base_params = {"f": "json"}
base_params.update(params)
response = logged_in_api_client.get(url, base_params)
assert response.status_code == 200
assert response.data == {
"albumList2": {
"album": serializers.get_album_list2_data([albums[i] for i in expected])
}
}
@pytest.mark.parametrize("f", ["json"])
@pytest.mark.parametrize(
"tags_field",
["set_tags", "artist__set_tags", "album__set_tags", "album__artist__set_tags"],
)
def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_songs_by_genre")
assert url.endswith("getSongsByGenre") is True
track1 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
track2 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
factories["music.Track"](playable=True, **{tags_field: ["Pop"]})
expected = {
"songsByGenre": {"song": serializers.get_song_list_data([track2, track1])}
}
response = logged_in_api_client.get(
url, {"f": f, "count": 5, "offset": 0, "genre": "rock"}
)
assert response.status_code == 200
assert response.data == expected
def test_get_songs_by_genre_offset(logged_in_api_client, factories):
url = reverse("api:subsonic-get_songs_by_genre")
assert url.endswith("getSongsByGenre") is True
track1 = factories["music.Track"](playable=True, set_tags=["Rock"])
factories["music.Track"](playable=True, set_tags=["Rock"])
factories["music.Track"](playable=True, set_tags=["Pop"])
# the API order tracks by creation date DESC, so the second one
# returned by the API is the first one to be created in the test.
expected = {"songsByGenre": {"song": serializers.get_song_list_data([track1])}}
response = logged_in_api_client.get(
url, {"f": "json", "count": 1, "offset": 1, "genre": "rock"}
)
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("f", ["json"])
def test_search3(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-search3")
assert url.endswith("search3") is True
artist = factories["music.Artist"](name="testvalue", playable=True)
factories["music.Artist"](name="nope2", playable=True)
album = factories["music.Album"](title="testvalue", playable=True)
factories["music.Album"](title="nope2", playable=True)
track = factories["music.Track"](title="testvalue", playable=True)
factories["music.Track"](title="nope2", playable=True)
response = logged_in_api_client.get(url, {"f": f, "query": "testval"})
artist_qs = (
music_models.Artist.objects.with_albums_count()
.filter(pk=artist.pk)
.values("_albums_count", "id", "name")
)
assert response.status_code == 200
assert response.data == {
"searchResult3": {
"artist": [serializers.get_artist_data(a) for a in artist_qs],
"album": serializers.get_album_list2_data([album]),
"song": serializers.get_song_list_data([track]),
@pytest.mark.parametrize("f", ["json"])
def test_get_playlists(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_playlists")
playlist1 = factories["playlists.PlaylistTrack"](
playlist__user=logged_in_api_client.user
).playlist
playlist2 = factories["playlists.PlaylistTrack"](
playlist__privacy_level="everyone"
).playlist
playlist3 = factories["playlists.PlaylistTrack"](
playlist__privacy_level="instance"
).playlist
# private
factories["playlists.PlaylistTrack"](playlist__privacy_level="me")
# no track
factories["playlists.Playlist"](privacy_level="everyone")
qs = (
playlist1.__class__.objects.with_tracks_count()
.filter(pk__in=[playlist1.pk, playlist2.pk, playlist3.pk])
.order_by("-creation_date")
)
expected = {
"playlists": {"playlist": [serializers.get_playlist_data(p) for p in qs]}
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("f", ["json"])
def test_get_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_playlist")
playlist = factories["playlists.PlaylistTrack"](
playlist__user=logged_in_api_client.user
).playlist
response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
qs = playlist.__class__.objects.with_tracks_count()
assert response.status_code == 200
assert response.data == {
"playlist": serializers.get_playlist_detail_data(qs.first())
@pytest.mark.parametrize("f", ["json"])
def test_update_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-update_playlist")
assert url.endswith("updatePlaylist") is True
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
factories["playlists.PlaylistTrack"](index=0, playlist=playlist)
response = logged_in_api_client.get(
url,
{
"f": f,
"name": "new_name",
"playlistId": playlist.pk,
"songIdToAdd": new_track.pk,
"songIndexToRemove": 0,
},
)
playlist.refresh_from_db()
assert response.status_code == 200
assert playlist.playlist_tracks.count() == 1
assert playlist.playlist_tracks.first().track_id == new_track.pk
@pytest.mark.parametrize("f", ["json"])
def test_delete_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-delete_playlist")
assert url.endswith("deletePlaylist") is True
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
assert response.status_code == 200
with pytest.raises(playlist.__class__.DoesNotExist):
playlist.refresh_from_db()
@pytest.mark.parametrize("f", ["json"])
def test_create_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-create_playlist")
assert url.endswith("createPlaylist") is True
track1 = factories["music.Track"]()
track2 = factories["music.Track"]()
response = logged_in_api_client.get(
url, {"f": f, "name": "hello", "songId": [track1.pk, track2.pk]}
)
assert response.status_code == 200
playlist = logged_in_api_client.user.playlists.latest("id")
assert playlist.playlist_tracks.count() == 2
for i, t in enumerate([track1, track2]):
plt = playlist.playlist_tracks.get(track=t)
assert plt.index == i
qs = playlist.__class__.objects.with_tracks_count()
assert response.data == {
"playlist": serializers.get_playlist_detail_data(qs.first())
@pytest.mark.parametrize("f", ["json"])
def test_get_music_folders(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_music_folders")
assert url.endswith("getMusicFolders") is True
response = logged_in_api_client.get(url, {"f": f})
assert response.status_code == 200
assert response.data == {
"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}
@pytest.mark.parametrize("f", ["json"])
def test_get_indexes(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
factories["moderation.UserFilter"](
user=logged_in_api_client.user,
target_artist=factories["music.Artist"](playable=True),
)
exclude_query = moderation_filters.get_filtered_content_query(
moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
)
factories["music.Artist"].create_batch(size=3, playable=True)
music_models.Artist.objects.all().exclude(exclude_query)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
response = logged_in_api_client.get(url)
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(
music_models.Artist.objects.all().exclude(exclude_query),
logged_in_api_client.user.actor,
)
def test_get_cover_art_album(factories, logged_in_api_client):
url = reverse("api:subsonic-get_cover_art")
album = factories["music.Album"](with_cover=True)
response = logged_in_api_client.get(url, {"id": "al-{}".format(album.pk)})
assert response.status_code == 200
assert response["Content-Type"] == ""
assert response["X-Accel-Redirect"] == music_views.get_file_path(
def test_get_cover_art_attachment(factories, logged_in_api_client):
attachment = factories["common.Attachment"]()
url = reverse("api:subsonic-get_cover_art")
assert url.endswith("getCoverArt") is True
response = logged_in_api_client.get(url, {"id": "at-{}".format(attachment.uuid)})
assert response.status_code == 200
assert response["Content-Type"] == ""
assert response["X-Accel-Redirect"] == music_views.get_file_path(
attachment.file
).decode("utf-8")
def test_get_avatar(factories, logged_in_api_client):
user = factories["users.User"]()
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
url = reverse("api:subsonic-scrobble")
assert url.endswith("scrobble") is True
response = logged_in_api_client.get(url, {"id": track.pk, "submission": True})
assert response.status_code == 200
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):
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",
"streamRole": "true",
"jukeboxRole": "true",
"coverArtRole": "false",
"shareRole": "false",
"folder": [
f["id"] for f in serializers.get_folders(logged_in_api_client.user)
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
def test_create_podcast_channel(logged_in_api_client, factories, mocker):
channel = factories["audio.Channel"](external=True)
rss_url = "https://rss.url/"
get_channel_from_rss_url = mocker.patch(
"funkwhale_api.audio.serializers.get_channel_from_rss_url",
return_value=(channel, []),
)
actor = logged_in_api_client.user.create_actor()
url = reverse("api:subsonic-create_podcast_channel")
assert url.endswith("createPodcastChannel") is True
response = logged_in_api_client.get(url, {"f": "json", "url": rss_url})
assert response.status_code == 200
assert response.data == {"status": "ok"}
subscription = actor.emitted_follows.get(target=channel.actor)
assert subscription.approved is True
get_channel_from_rss_url.assert_called_once_with(rss_url)
def test_delete_podcast_channel(logged_in_api_client, factories, mocker):
actor = logged_in_api_client.user.create_actor()
channel = factories["audio.Channel"](external=True)
subscription = factories["federation.Follow"](actor=actor, target=channel.actor)
other_subscription = factories["federation.Follow"](target=channel.actor)
url = reverse("api:subsonic-delete_podcast_channel")
assert url.endswith("deletePodcastChannel") is True
response = logged_in_api_client.get(url, {"f": "json", "id": channel.uuid})
assert response.status_code == 200
assert response.data == {"status": "ok"}
other_subscription.refresh_from_db()
with pytest.raises(subscription.DoesNotExist):
subscription.refresh_from_db()
def test_get_podcasts(logged_in_api_client, factories, mocker):
actor = logged_in_api_client.user.create_actor()
channel = factories["audio.Channel"](
external=True, library__privacy_level="everyone"
)
upload1 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
library=channel.library,
bitrate=128000,
duration=42,
)
upload2 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
library=channel.library,
bitrate=256000,
duration=43,
)
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
factories["music.Upload"](import_status="pending", track__artist=channel.artist)
factories["audio.Channel"](external=True)
factories["federation.Follow"]()
url = reverse("api:subsonic-get_podcasts")
assert url.endswith("getPodcasts") is True
response = logged_in_api_client.get(url, {"f": "json"})
assert response.status_code == 200
assert response.data == {
"podcasts": {
"channel": [serializers.get_channel_data(channel, [upload2, upload1])],
}
}
def test_get_podcasts_by_id(logged_in_api_client, factories, mocker):
actor = logged_in_api_client.user.create_actor()
channel1 = factories["audio.Channel"](
external=True, library__privacy_level="everyone"
)
channel2 = factories["audio.Channel"](
external=True, library__privacy_level="everyone"
)
upload1 = factories["music.Upload"](
playable=True,
track__artist=channel1.artist,
library=channel1.library,
bitrate=128000,
duration=42,
)
factories["music.Upload"](
playable=True,
track__artist=channel2.artist,
library=channel2.library,
bitrate=256000,
duration=43,
)
factories["federation.Follow"](actor=actor, target=channel1.actor, approved=True)
factories["federation.Follow"](actor=actor, target=channel2.actor, approved=True)
url = reverse("api:subsonic-get_podcasts")
assert url.endswith("getPodcasts") is True
response = logged_in_api_client.get(url, {"f": "json", "id": channel1.uuid})
assert response.status_code == 200
assert response.data == {
"podcasts": {"channel": [serializers.get_channel_data(channel1, [upload1])]}
}
def test_get_newest_podcasts(logged_in_api_client, factories, mocker):
actor = logged_in_api_client.user.create_actor()
channel = factories["audio.Channel"](
external=True, library__privacy_level="everyone"
)
upload1 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
library=channel.library,
bitrate=128000,
duration=42,
)
upload2 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
library=channel.library,
bitrate=256000,
duration=43,
)
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
url = reverse("api:subsonic-get_newest_podcasts")
assert url.endswith("getNewestPodcasts") is True
response = logged_in_api_client.get(url, {"f": "json"})
assert response.status_code == 200
assert response.data == {
"newestPodcasts": {
"episode": [
serializers.get_channel_episode_data(upload, channel.uuid)
for upload in [upload2, upload1]
],
}
}