Merge branch 'subsonic-compat' into 'develop'
Subsonic compat See merge request funkwhale/funkwhale!943
This commit is contained in:
commit
346ddba262
|
@ -8,6 +8,40 @@ from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.music import utils as music_utils
|
from funkwhale_api.music import utils as music_utils
|
||||||
|
|
||||||
|
|
||||||
|
def to_subsonic_date(date):
|
||||||
|
"""
|
||||||
|
Subsonic expects this kind of date format: 2012-04-17T19:55:49.000Z
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not date:
|
||||||
|
return
|
||||||
|
|
||||||
|
return date.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||||||
|
|
||||||
|
|
||||||
|
def get_valid_filepart(s):
|
||||||
|
"""
|
||||||
|
Return a string suitable for use in a file path. Escape most non-ASCII
|
||||||
|
chars, and truncate the string to a suitable length too.
|
||||||
|
"""
|
||||||
|
max_length = 50
|
||||||
|
keepcharacters = " ._()[]-+"
|
||||||
|
final = "".join(
|
||||||
|
c if c.isalnum() or c in keepcharacters else "_" for c in s
|
||||||
|
).rstrip()
|
||||||
|
return final[:max_length]
|
||||||
|
|
||||||
|
|
||||||
|
def get_track_path(track, suffix):
|
||||||
|
artist_part = get_valid_filepart(track.artist.name)
|
||||||
|
album_part = get_valid_filepart(track.album.title)
|
||||||
|
track_part = get_valid_filepart(track.title) + "." + suffix
|
||||||
|
if track.position:
|
||||||
|
track_part = "{} - {}".format(track.position, track_part)
|
||||||
|
|
||||||
|
return "/".join([artist_part, album_part, track_part])
|
||||||
|
|
||||||
|
|
||||||
def get_artist_data(artist_values):
|
def get_artist_data(artist_values):
|
||||||
return {
|
return {
|
||||||
"id": artist_values["id"],
|
"id": artist_values["id"],
|
||||||
|
@ -52,7 +86,7 @@ class GetArtistSerializer(serializers.Serializer):
|
||||||
"artistId": artist.id,
|
"artistId": artist.id,
|
||||||
"name": album.title,
|
"name": album.title,
|
||||||
"artist": artist.name,
|
"artist": artist.name,
|
||||||
"created": album.creation_date,
|
"created": to_subsonic_date(album.creation_date),
|
||||||
"songCount": len(album.tracks.all()),
|
"songCount": len(album.tracks.all()),
|
||||||
}
|
}
|
||||||
if album.cover:
|
if album.cover:
|
||||||
|
@ -81,8 +115,9 @@ def get_track_data(album, track, upload):
|
||||||
else "audio/mpeg"
|
else "audio/mpeg"
|
||||||
),
|
),
|
||||||
"suffix": upload.extension or "",
|
"suffix": upload.extension or "",
|
||||||
|
"path": get_track_path(track, upload.extension or "mp3"),
|
||||||
"duration": upload.duration or 0,
|
"duration": upload.duration or 0,
|
||||||
"created": track.creation_date,
|
"created": to_subsonic_date(track.creation_date),
|
||||||
"albumId": album.pk,
|
"albumId": album.pk,
|
||||||
"artistId": album.artist.pk,
|
"artistId": album.artist.pk,
|
||||||
"type": "music",
|
"type": "music",
|
||||||
|
@ -104,7 +139,7 @@ def get_album2_data(album):
|
||||||
"artistId": album.artist.id,
|
"artistId": album.artist.id,
|
||||||
"name": album.title,
|
"name": album.title,
|
||||||
"artist": album.artist.name,
|
"artist": album.artist.name,
|
||||||
"created": album.creation_date,
|
"created": to_subsonic_date(album.creation_date),
|
||||||
}
|
}
|
||||||
if album.cover:
|
if album.cover:
|
||||||
payload["coverArt"] = "al-{}".format(album.id)
|
payload["coverArt"] = "al-{}".format(album.id)
|
||||||
|
@ -162,7 +197,7 @@ def get_starred_tracks_data(favorites):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
continue
|
continue
|
||||||
td = get_track_data(t.album, t, uploads)
|
td = get_track_data(t.album, t, uploads)
|
||||||
td["starred"] = by_track_id[t.pk].creation_date
|
td["starred"] = to_subsonic_date(by_track_id[t.pk].creation_date)
|
||||||
data.append(td)
|
data.append(td)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -179,7 +214,7 @@ def get_playlist_data(playlist):
|
||||||
"public": "false",
|
"public": "false",
|
||||||
"songCount": playlist._tracks_count,
|
"songCount": playlist._tracks_count,
|
||||||
"duration": 0,
|
"duration": 0,
|
||||||
"created": playlist.creation_date,
|
"created": to_subsonic_date(playlist.creation_date),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,8 +255,9 @@ def get_music_directory_data(artist):
|
||||||
"year": track.album.release_date.year if track.album.release_date else 0,
|
"year": track.album.release_date.year if track.album.release_date else 0,
|
||||||
"contentType": upload.mimetype,
|
"contentType": upload.mimetype,
|
||||||
"suffix": upload.extension or "",
|
"suffix": upload.extension or "",
|
||||||
|
"path": get_track_path(track, upload.extension or "mp3"),
|
||||||
"duration": upload.duration or 0,
|
"duration": upload.duration or 0,
|
||||||
"created": track.creation_date,
|
"created": to_subsonic_date(track.creation_date),
|
||||||
"albumId": album.pk,
|
"albumId": album.pk,
|
||||||
"artistId": artist.pk,
|
"artistId": artist.pk,
|
||||||
"parent": artist.id,
|
"parent": artist.id,
|
||||||
|
@ -259,7 +295,7 @@ def get_user_detail_data(user):
|
||||||
"playlistRole": "true",
|
"playlistRole": "true",
|
||||||
"streamRole": "true",
|
"streamRole": "true",
|
||||||
"jukeboxRole": "true",
|
"jukeboxRole": "true",
|
||||||
"folder": [f["id"] for f in get_folders(user)],
|
"folder": [{"value": f["id"]} for f in get_folders(user)],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,71 @@
|
||||||
|
import datetime
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from funkwhale_api.music import models as music_models
|
from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.subsonic import serializers
|
from funkwhale_api.subsonic import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"date, expected",
|
||||||
|
[
|
||||||
|
(datetime.datetime(2017, 1, 12, 9, 53, 12, 1890), "2017-01-12T09:53:12.000Z"),
|
||||||
|
(None, None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_to_subsonic_date(date, expected):
|
||||||
|
assert serializers.to_subsonic_date(date) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input, expected",
|
||||||
|
[
|
||||||
|
("AC/DC", "AC_DC"),
|
||||||
|
("AC-DC", "AC-DC"),
|
||||||
|
("A" * 100, "A" * 50),
|
||||||
|
("Album (2019)", "Album (2019)"),
|
||||||
|
("Haven't", "Haven_t"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_valid_filepart(input, expected):
|
||||||
|
assert serializers.get_valid_filepart(input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"factory_kwargs, suffix, expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"artist__name": "Hello",
|
||||||
|
"album__title": "World",
|
||||||
|
"title": "foo",
|
||||||
|
"position": None,
|
||||||
|
},
|
||||||
|
"mp3",
|
||||||
|
"Hello/World/foo.mp3",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"artist__name": "AC/DC",
|
||||||
|
"album__title": "escape/my",
|
||||||
|
"title": "sla/sh",
|
||||||
|
"position": 12,
|
||||||
|
},
|
||||||
|
"ogg",
|
||||||
|
"/".join(
|
||||||
|
[
|
||||||
|
serializers.get_valid_filepart("AC/DC"),
|
||||||
|
serializers.get_valid_filepart("escape/my"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ "/12 - {}.ogg".format(serializers.get_valid_filepart("sla/sh")),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_track_path(factory_kwargs, suffix, expected, factories):
|
||||||
|
track = factories["music.Track"](**factory_kwargs)
|
||||||
|
assert serializers.get_track_path(track, suffix) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_get_artists_serializer(factories):
|
def test_get_artists_serializer(factories):
|
||||||
artist1 = factories["music.Artist"](name="eliot")
|
artist1 = factories["music.Artist"](name="eliot")
|
||||||
artist2 = factories["music.Artist"](name="Ellena")
|
artist2 = factories["music.Artist"](name="Ellena")
|
||||||
|
@ -54,7 +116,7 @@ def test_get_artist_serializer(factories):
|
||||||
"name": album.title,
|
"name": album.title,
|
||||||
"artist": artist.name,
|
"artist": artist.name,
|
||||||
"songCount": len(tracks),
|
"songCount": len(tracks),
|
||||||
"created": album.creation_date,
|
"created": serializers.to_subsonic_date(album.creation_date),
|
||||||
"year": album.release_date.year,
|
"year": album.release_date.year,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -96,7 +158,7 @@ def test_get_album_serializer(factories):
|
||||||
"name": album.title,
|
"name": album.title,
|
||||||
"artist": artist.name,
|
"artist": artist.name,
|
||||||
"songCount": 1,
|
"songCount": 1,
|
||||||
"created": album.creation_date,
|
"created": serializers.to_subsonic_date(album.creation_date),
|
||||||
"year": album.release_date.year,
|
"year": album.release_date.year,
|
||||||
"coverArt": "al-{}".format(album.id),
|
"coverArt": "al-{}".format(album.id),
|
||||||
"song": [
|
"song": [
|
||||||
|
@ -112,10 +174,11 @@ def test_get_album_serializer(factories):
|
||||||
"year": track.album.release_date.year,
|
"year": track.album.release_date.year,
|
||||||
"contentType": upload.mimetype,
|
"contentType": upload.mimetype,
|
||||||
"suffix": upload.extension or "",
|
"suffix": upload.extension or "",
|
||||||
|
"path": serializers.get_track_path(track, upload.extension),
|
||||||
"bitrate": 42,
|
"bitrate": 42,
|
||||||
"duration": 43,
|
"duration": 43,
|
||||||
"size": 44,
|
"size": 44,
|
||||||
"created": track.creation_date,
|
"created": serializers.to_subsonic_date(track.creation_date),
|
||||||
"albumId": album.pk,
|
"albumId": album.pk,
|
||||||
"artistId": artist.pk,
|
"artistId": artist.pk,
|
||||||
"type": "music",
|
"type": "music",
|
||||||
|
@ -133,7 +196,7 @@ def test_starred_tracks2_serializer(factories):
|
||||||
upload = factories["music.Upload"](track=track)
|
upload = factories["music.Upload"](track=track)
|
||||||
favorite = factories["favorites.TrackFavorite"](track=track)
|
favorite = factories["favorites.TrackFavorite"](track=track)
|
||||||
expected = [serializers.get_track_data(album, track, upload)]
|
expected = [serializers.get_track_data(album, track, upload)]
|
||||||
expected[0]["starred"] = favorite.creation_date
|
expected[0]["starred"] = serializers.to_subsonic_date(favorite.creation_date)
|
||||||
data = serializers.get_starred_tracks_data([favorite])
|
data = serializers.get_starred_tracks_data([favorite])
|
||||||
assert data == expected
|
assert data == expected
|
||||||
|
|
||||||
|
@ -162,7 +225,7 @@ def test_playlist_serializer(factories):
|
||||||
"public": "false",
|
"public": "false",
|
||||||
"songCount": 1,
|
"songCount": 1,
|
||||||
"duration": 0,
|
"duration": 0,
|
||||||
"created": playlist.creation_date,
|
"created": serializers.to_subsonic_date(playlist.creation_date),
|
||||||
}
|
}
|
||||||
qs = playlist.__class__.objects.with_tracks_count()
|
qs = playlist.__class__.objects.with_tracks_count()
|
||||||
data = serializers.get_playlist_data(qs.first())
|
data = serializers.get_playlist_data(qs.first())
|
||||||
|
@ -181,7 +244,7 @@ def test_playlist_detail_serializer(factories):
|
||||||
"public": "false",
|
"public": "false",
|
||||||
"songCount": 1,
|
"songCount": 1,
|
||||||
"duration": 0,
|
"duration": 0,
|
||||||
"created": playlist.creation_date,
|
"created": serializers.to_subsonic_date(playlist.creation_date),
|
||||||
"entry": [serializers.get_track_data(plt.track.album, plt.track, upload)],
|
"entry": [serializers.get_track_data(plt.track.album, plt.track, upload)],
|
||||||
}
|
}
|
||||||
qs = playlist.__class__.objects.with_tracks_count()
|
qs = playlist.__class__.objects.with_tracks_count()
|
||||||
|
@ -210,10 +273,11 @@ def test_directory_serializer_artist(factories):
|
||||||
"year": track.album.release_date.year,
|
"year": track.album.release_date.year,
|
||||||
"contentType": upload.mimetype,
|
"contentType": upload.mimetype,
|
||||||
"suffix": upload.extension or "",
|
"suffix": upload.extension or "",
|
||||||
|
"path": serializers.get_track_path(track, upload.extension),
|
||||||
"bitrate": 42,
|
"bitrate": 42,
|
||||||
"duration": 43,
|
"duration": 43,
|
||||||
"size": 44,
|
"size": 44,
|
||||||
"created": track.creation_date,
|
"created": serializers.to_subsonic_date(track.creation_date),
|
||||||
"albumId": album.pk,
|
"albumId": album.pk,
|
||||||
"artistId": artist.pk,
|
"artistId": artist.pk,
|
||||||
"parent": artist.pk,
|
"parent": artist.pk,
|
||||||
|
|
|
@ -762,7 +762,8 @@ def test_get_user(f, db, logged_in_api_client, factories):
|
||||||
"coverArtRole": "false",
|
"coverArtRole": "false",
|
||||||
"shareRole": "false",
|
"shareRole": "false",
|
||||||
"folder": [
|
"folder": [
|
||||||
f["id"] for f in serializers.get_folders(logged_in_api_client.user)
|
{"value": f["id"]}
|
||||||
|
for f in serializers.get_folders(logged_in_api_client.user)
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue