diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index 5a8e7f37e..8e881f24d 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -19,6 +19,29 @@ def to_subsonic_date(date): 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): return { "id": artist_values["id"], @@ -92,6 +115,7 @@ def get_track_data(album, track, upload): else "audio/mpeg" ), "suffix": upload.extension or "", + "path": get_track_path(track, upload.extension or "mp3"), "duration": upload.duration or 0, "created": to_subsonic_date(track.creation_date), "albumId": album.pk, @@ -231,6 +255,7 @@ def get_music_directory_data(artist): "year": track.album.release_date.year if track.album.release_date else 0, "contentType": upload.mimetype, "suffix": upload.extension or "", + "path": get_track_path(track, upload.extension or "mp3"), "duration": upload.duration or 0, "created": to_subsonic_date(track.creation_date), "albumId": album.pk, diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index 9998e6ef2..4da84ec35 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -16,6 +16,56 @@ 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): artist1 = factories["music.Artist"](name="eliot") artist2 = factories["music.Artist"](name="Ellena") @@ -124,6 +174,7 @@ def test_get_album_serializer(factories): "year": track.album.release_date.year, "contentType": upload.mimetype, "suffix": upload.extension or "", + "path": serializers.get_track_path(track, upload.extension), "bitrate": 42, "duration": 43, "size": 44, @@ -222,6 +273,7 @@ def test_directory_serializer_artist(factories): "year": track.album.release_date.year, "contentType": upload.mimetype, "suffix": upload.extension or "", + "path": serializers.get_track_path(track, upload.extension), "bitrate": 42, "duration": 43, "size": 44,