Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
Show changes
Showing
with 677 additions and 536 deletions
...@@ -42,7 +42,7 @@ def structure_payload(data): ...@@ -42,7 +42,7 @@ def structure_payload(data):
"status": "ok", "status": "ok",
"type": "funkwhale", "type": "funkwhale",
"version": "1.16.0", "version": "1.16.0",
"openSubsonic": "true", "openSubsonic": True,
} }
payload.update(data) payload.update(data)
if "detail" in payload: if "detail" in payload:
...@@ -70,6 +70,7 @@ class SubsonicXMLRenderer(renderers.JSONRenderer): ...@@ -70,6 +70,7 @@ class SubsonicXMLRenderer(renderers.JSONRenderer):
return super().render(data, accepted_media_type, renderer_context) return super().render(data, accepted_media_type, renderer_context)
final = structure_payload(data) final = structure_payload(data)
final["xmlns"] = "http://subsonic.org/restapi" final["xmlns"] = "http://subsonic.org/restapi"
final["openSubsonic"] = "true"
tree = dict_to_xml_tree("subsonic-response", final) tree = dict_to_xml_tree("subsonic-response", final)
return b'<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring( return b'<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
tree, encoding="utf-8" tree, encoding="utf-8"
......
...@@ -111,6 +111,9 @@ class GetArtistInfo2Serializer(serializers.Serializer): ...@@ -111,6 +111,9 @@ class GetArtistInfo2Serializer(serializers.Serializer):
if artist.mbid: if artist.mbid:
payload["musicBrainzId"] = TagValue(artist.mbid) payload["musicBrainzId"] = TagValue(artist.mbid)
if artist.attachment_cover: if artist.attachment_cover:
payload["smallImageUrl"] = TagValue(
artist.attachment_cover.download_url_small_square_crop
)
payload["mediumImageUrl"] = TagValue( payload["mediumImageUrl"] = TagValue(
artist.attachment_cover.download_url_medium_square_crop artist.attachment_cover.download_url_medium_square_crop
) )
...@@ -226,6 +229,28 @@ class GetSongSerializer(serializers.Serializer): ...@@ -226,6 +229,28 @@ class GetSongSerializer(serializers.Serializer):
return get_track_data(track.album, track, uploads[0]) return get_track_data(track.album, track, uploads[0])
class GetTopSongsSerializer(serializers.Serializer):
def to_representation(self, artist):
top_tracks = (
history_models.Listening.objects.filter(track__artist_credit__artist=artist)
.values("track")
.annotate(listen_count=Count("id"))
.order_by("-listen_count")[: self.context["count"]]
)
if not len(top_tracks):
return {}
top_tracks_instances = []
for track in top_tracks:
track = music_models.Track.objects.get(id=track["track"])
top_tracks_instances.append(track)
return [
get_track_data(track.album, track, track.uploads.all()[0])
for track in top_tracks_instances
]
def get_starred_tracks_data(favorites): def get_starred_tracks_data(favorites):
by_track_id = {f.track_id: f for f in favorites} by_track_id = {f.track_id: f for f in favorites}
tracks = ( tracks = (
...@@ -335,15 +360,21 @@ def get_channel_data(channel, uploads): ...@@ -335,15 +360,21 @@ def get_channel_data(channel, uploads):
"id": str(channel.uuid), "id": str(channel.uuid),
"url": channel.get_rss_url(), "url": channel.get_rss_url(),
"title": channel.artist.name, "title": channel.artist.name,
"description": channel.artist.description.as_plain_text "description": (
channel.artist.description.as_plain_text
if channel.artist.description if channel.artist.description
else "", else ""
"coverArt": f"at-{channel.artist.attachment_cover.uuid}" ),
"coverArt": (
f"at-{channel.artist.attachment_cover.uuid}"
if channel.artist.attachment_cover if channel.artist.attachment_cover
else "", else ""
"originalImageUrl": channel.artist.attachment_cover.url ),
"originalImageUrl": (
channel.artist.attachment_cover.url
if channel.artist.attachment_cover if channel.artist.attachment_cover
else "", else ""
),
"status": "completed", "status": "completed",
} }
if uploads: if uploads:
...@@ -360,12 +391,14 @@ def get_channel_episode_data(upload, channel_id): ...@@ -360,12 +391,14 @@ def get_channel_episode_data(upload, channel_id):
"channelId": str(channel_id), "channelId": str(channel_id),
"streamId": upload.track.id, "streamId": upload.track.id,
"title": upload.track.title, "title": upload.track.title,
"description": upload.track.description.as_plain_text "description": (
if upload.track.description upload.track.description.as_plain_text if upload.track.description else ""
else "", ),
"coverArt": f"at-{upload.track.attachment_cover.uuid}" "coverArt": (
f"at-{upload.track.attachment_cover.uuid}"
if upload.track.attachment_cover if upload.track.attachment_cover
else "", else ""
),
"isDir": "false", "isDir": "false",
"year": upload.track.creation_date.year, "year": upload.track.creation_date.year,
"publishDate": upload.track.creation_date.isoformat(), "publishDate": upload.track.creation_date.isoformat(),
......
""" """
Documentation of Subsonic API can be found at http://www.subsonic.org/pages/api.jsp Documentation of Subsonic API can be found at http://www.subsonic.org/pages/api.jsp
""" """
import datetime import datetime
import functools import functools
...@@ -90,6 +91,8 @@ def find_object( ...@@ -90,6 +91,8 @@ def find_object(
} }
} }
) )
except qs.model.MultipleObjectsReturned:
obj = qs.filter(**{model_field: value})[0]
kwargs["obj"] = obj kwargs["obj"] = obj
return func(self, request, *args, **kwargs) return func(self, request, *args, **kwargs)
...@@ -260,6 +263,43 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -260,6 +263,43 @@ class SubsonicViewSet(viewsets.GenericViewSet):
return response.Response(payload, status=200) return response.Response(payload, status=200)
# This should return last.fm data but we choose to return the pod top song
@action(
detail=False,
methods=["get", "post"],
url_name="get_top_songs",
url_path="getTopSongs",
)
@find_object(
music_models.Artist.objects.all(),
model_field="artist_credit__artist__name",
field="artist",
filter_playable=True,
cast=str,
)
def get_top_songs(self, request, *args, **kwargs):
artist = kwargs.pop("obj")
data = request.GET or request.POST
try:
count = int(data["count"])
except KeyError:
return response.Response(
{
"error": {
"code": 10,
"message": "required parameter 'count' not present",
}
}
)
# passing with many=true to make the serializer accept the returned list
data = serializers.GetTopSongsSerializer(
[artist], context={"count": count}, many=True
).data
payload = {"topSongs": data[0]}
return response.Response(payload, status=200)
@action( @action(
detail=False, detail=False,
methods=["get", "post"], methods=["get", "post"],
...@@ -289,6 +329,44 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -289,6 +329,44 @@ class SubsonicViewSet(viewsets.GenericViewSet):
payload = {"album": data} payload = {"album": data}
return response.Response(payload, status=200) return response.Response(payload, status=200)
# A clone of get_album (this should return last.fm data but we prefer to send our own metadata)
@action(
detail=False,
methods=["get", "post"],
url_name="get_album_info_2",
url_path="getAlbumInfo2",
)
@find_object(
music_models.Album.objects.with_duration().prefetch_related(
"artist_credit__artist"
),
filter_playable=True,
)
def get_album_info_2(self, request, *args, **kwargs):
album = kwargs.pop("obj")
data = serializers.GetAlbumSerializer(album).data
payload = {"albumInfo": data}
return response.Response(payload, status=200)
# A clone of get_album (this should return last.fm data but we prefer to send our own metadata)
@action(
detail=False,
methods=["get", "post"],
url_name="get_album_info",
url_path="getAlbumInfo",
)
@find_object(
music_models.Album.objects.with_duration().prefetch_related(
"artist_credit__artist"
),
filter_playable=True,
)
def get_album_info(self, request, *args, **kwargs):
album = kwargs.pop("obj")
data = serializers.GetAlbumSerializer(album).data
payload = {"albumInfo": data}
return response.Response(payload, status=200)
@action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream") @action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream")
@find_object(music_models.Track.objects.all(), filter_playable=True) @find_object(music_models.Track.objects.all(), filter_playable=True)
def stream(self, request, *args, **kwargs): def stream(self, request, *args, **kwargs):
...@@ -815,7 +893,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -815,7 +893,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
.select_related("attachment_cover") .select_related("attachment_cover")
.get(pk=artist_id) .get(pk=artist_id)
) )
except (TypeError, ValueError, music_models.Album.DoesNotExist): except (TypeError, ValueError, music_models.Artist.DoesNotExist):
return response.Response( return response.Response(
{"error": {"code": 70, "message": "cover art not found."}} {"error": {"code": 70, "message": "cover art not found."}}
) )
...@@ -824,7 +902,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): ...@@ -824,7 +902,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
try: try:
attachment_id = id.replace("at-", "") attachment_id = id.replace("at-", "")
attachment = common_models.Attachment.objects.get(uuid=attachment_id) attachment = common_models.Attachment.objects.get(uuid=attachment_id)
except (TypeError, ValueError, music_models.Album.DoesNotExist): except (TypeError, ValueError, common_models.Attachment.DoesNotExist):
return response.Response( return response.Response(
{"error": {"code": 70, "message": "cover art not found."}} {"error": {"code": 70, "message": "cover art not found."}}
) )
......
...@@ -471,7 +471,7 @@ def create_user_libraries(user): ...@@ -471,7 +471,7 @@ def create_user_libraries(user):
uuid=(new_uuid := uuid.uuid4()), uuid=(new_uuid := uuid.uuid4()),
fid=federation_utils.full_url( fid=federation_utils.full_url(
reverse( reverse(
"federation:music:playlists-detail", "federation:music:libraries-detail",
kwargs={"uuid": new_uuid}, kwargs={"uuid": new_uuid},
) )
), ),
......
This diff is collapsed.
[tool.poetry] [tool.poetry]
name = "funkwhale-api" name = "funkwhale-api"
version = "1.4.0" version = "2.0.0-alpha.2"
description = "Funkwhale API" description = "Funkwhale API"
authors = ["Funkwhale Collective"] authors = ["Funkwhale Collective"]
...@@ -25,29 +25,29 @@ exclude = ["tests"] ...@@ -25,29 +25,29 @@ exclude = ["tests"]
funkwhale-manage = 'funkwhale_api.main:main' funkwhale-manage = 'funkwhale_api.main:main'
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10,<3.14" python = "^3.11,<3.14"
# Django # Django
dj-rest-auth = "7.0.1" dj-rest-auth = "7.0.1"
django = "5.1.5" django = "5.1.6"
django-allauth = "65.3.1" django-allauth = "65.4.1"
django-cache-memoize = "0.2.1" django-cache-memoize = "0.2.1"
django-cacheops = "==7.1" django-cacheops = "==7.1"
django-cleanup = "==9.0.0" django-cleanup = "==9.0.0"
django-cors-headers = "==4.6.0" django-cors-headers = "==4.7.0"
django-dynamic-preferences = "==1.17.0" django-dynamic-preferences = "==1.17.0"
django-environ = "==0.12.0" django-environ = "==0.12.0"
django-filter = "==24.3" django-filter = "==25.1"
django-oauth-toolkit = "3.0.1" django-oauth-toolkit = "3.0.1"
django-redis = "==5.4.0" django-redis = "==5.4.0"
django-storages = "==1.14.4" django-storages = "==1.14.5"
django-versatileimagefield = "==3.1" django-versatileimagefield = "==3.1"
djangorestframework = "==3.15.2" djangorestframework = "==3.15.2"
drf-spectacular = "==0.28.0" drf-spectacular = "==0.28.0"
markdown = "==3.7" markdown = "==3.7"
persisting-theory = "==1.0" persisting-theory = "==1.0"
psycopg2-binary = "==2.9.10" psycopg2-binary = "==2.9.10"
redis = "==5.2.1" redis = "==6.1.0"
# Django LDAP # Django LDAP
django-auth-ldap = "==5.1.0" django-auth-ldap = "==5.1.0"
...@@ -66,17 +66,17 @@ gunicorn = "==23.0.0" ...@@ -66,17 +66,17 @@ gunicorn = "==23.0.0"
uvicorn = { version = "==0.34.0", extras = ["standard"] } uvicorn = { version = "==0.34.0", extras = ["standard"] }
# Libs # Libs
aiohttp = "3.11.11" aiohttp = "3.11.12"
arrow = "==1.3.0" arrow = "==1.3.0"
backports-zoneinfo = { version = "==0.2.1", python = "<3.9" } backports-zoneinfo = { version = "==0.2.1", python = "<3.9" }
bleach = "==6.2.0" bleach = "==6.2.0"
boto3 = "==1.35.99" boto3 = "==1.36.21"
click = "==8.1.8" click = "==8.1.8"
cryptography = "==44.0.0" cryptography = "==44.0.1"
defusedxml = "0.7.1" defusedxml = "0.7.1"
feedparser = "==6.0.11" feedparser = "==6.0.11"
python-ffmpeg = "==2.0.12" python-ffmpeg = "==2.0.12"
liblistenbrainz = "==0.5.5" liblistenbrainz = "==0.5.6"
musicbrainzngs = "==0.7.1" musicbrainzngs = "==0.7.1"
mutagen = "==1.46.0" mutagen = "==1.46.0"
pillow = "==11.1.0" pillow = "==11.1.0"
...@@ -84,47 +84,47 @@ pyld = "==2.0.4" ...@@ -84,47 +84,47 @@ pyld = "==2.0.4"
python-magic = "==0.4.27" python-magic = "==0.4.27"
requests = "==2.32.3" requests = "==2.32.3"
requests-http-message-signatures = "==0.3.1" requests-http-message-signatures = "==0.3.1"
sentry-sdk = "==2.20.0" sentry-sdk = "==2.22.0"
watchdog = "==6.0.0" watchdog = "==6.0.0"
troi = "==2025.1.10.0" troi = "==2025.1.29.0"
lb-matching-tools = "==2024.1.30.1" lb-matching-tools = "==2024.1.30.1"
unidecode = "==1.3.8" unidecode = "==1.3.8"
pycountry = "24.6.1" pycountry = "24.6.1"
# Typesense # Typesense
typesense = { version = "==0.21.0", optional = true } typesense = { version = "==1.0.3", optional = true }
# Dependencies pinning # Dependencies pinning
ipython = "==8.31.0" ipython = "==9.2.0"
pluralizer = "==1.2.0" pluralizer = "==1.2.0"
service-identity = "==24.2.0" service-identity = "==24.2.0"
unicode-slugify = "==0.1.5" unicode-slugify = "==0.1.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
aioresponses = "==0.7.7" aioresponses = "==0.7.8"
asynctest = "==0.13.0" asynctest = "==0.13.0"
black = "==24.10.0" black = "==25.1.0"
coverage = { version = "==7.6.10", extras = ["toml"] } coverage = { version = "==7.6.12", extras = ["toml"] }
debugpy = "==1.8.11" debugpy = "==1.8.12"
django-coverage-plugin = "==3.1.0" django-coverage-plugin = "==3.1.0"
django-debug-toolbar = "==5.0.1" django-debug-toolbar = "==5.0.1"
factory-boy = "==3.3.1" factory-boy = "==3.3.3"
faker = "==33.3.1" faker = "==37.1.0"
flake8 = "==7.1.1" flake8 = "==7.1.2"
ipdb = "==0.13.13" ipdb = "==0.13.13"
pytest = "==8.3.4" pytest = "==8.3.4"
pytest-asyncio = "==0.25.2" pytest-asyncio = "==0.25.3"
prompt-toolkit = "==3.0.48" prompt-toolkit = "==3.0.50"
pytest-cov = "==6.0.0" pytest-cov = "==6.0.0"
pytest-django = "==4.9.0" pytest-django = "==4.10.0"
pytest-env = "==1.1.5" pytest-env = "==1.1.5"
pytest-mock = "==3.14.0" pytest-mock = "==3.14.0"
pytest-randomly = "==3.16.0" pytest-randomly = "==3.16.0"
pytest-sugar = "==1.0.0" pytest-sugar = "==1.0.0"
requests-mock = "==1.12.1" requests-mock = "==1.12.1"
pylint = "==3.3.3" pylint = "==3.3.4"
pylint-django = "==2.6.1" pylint-django = "==2.6.1"
django-extensions = "==3.2.3" django-extensions = "==4.1"
[tool.poetry.extras] [tool.poetry.extras]
typesense = ["typesense"] typesense = ["typesense"]
...@@ -135,7 +135,8 @@ build-backend = "poetry.core.masonry.api" ...@@ -135,7 +135,8 @@ build-backend = "poetry.core.masonry.api"
[tool.pylint.master] [tool.pylint.master]
load-plugins = ["pylint_django"] load-plugins = ["pylint_django"]
django-settings-module = "config.settings.testing" django-settings-module = "config.settings.local"
init-hook = 'import os; os.environ.setdefault("FUNKWHALE_URL", "https://test.federation")'
[tool.pylint.messages_control] [tool.pylint.messages_control]
disable = [ disable = [
......
...@@ -230,6 +230,7 @@ def test_channel_serializer_representation(factories, to_api_date): ...@@ -230,6 +230,7 @@ def test_channel_serializer_representation(factories, to_api_date):
"rss_url": channel.get_rss_url(), "rss_url": channel.get_rss_url(),
"url": channel.actor.url, "url": channel.actor.url,
"downloads_count": 12, "downloads_count": 12,
"subscriptions_count": 0,
} }
expected["artist"]["description"] = common_serializers.ContentSerializer( expected["artist"]["description"] = common_serializers.ContentSerializer(
content content
...@@ -254,6 +255,7 @@ def test_channel_serializer_external_representation(factories, to_api_date): ...@@ -254,6 +255,7 @@ def test_channel_serializer_external_representation(factories, to_api_date):
"rss_url": channel.get_rss_url(), "rss_url": channel.get_rss_url(),
"url": channel.actor.url, "url": channel.actor.url,
"downloads_count": 0, "downloads_count": 0,
"subscriptions_count": 0,
} }
expected["artist"]["description"] = common_serializers.ContentSerializer( expected["artist"]["description"] = common_serializers.ContentSerializer(
content content
......
...@@ -22,7 +22,7 @@ def test_mutation_fid_is_populated(factories, model, factory_args, namespace): ...@@ -22,7 +22,7 @@ def test_mutation_fid_is_populated(factories, model, factory_args, namespace):
("music.Artist", "/library/artists/{obj.pk}"), ("music.Artist", "/library/artists/{obj.pk}"),
("music.Album", "/library/albums/{obj.pk}"), ("music.Album", "/library/albums/{obj.pk}"),
("music.Track", "/library/tracks/{obj.pk}"), ("music.Track", "/library/tracks/{obj.pk}"),
("playlists.Playlist", "/library/playlists/{obj.pk}"), ("playlists.Playlist", "/library/playlists/{obj.uuid}"),
], ],
) )
def test_get_absolute_url(factory_name, factories, expected): def test_get_absolute_url(factory_name, factories, expected):
......
...@@ -98,9 +98,10 @@ def test_privacylevel_permission_me( ...@@ -98,9 +98,10 @@ def test_privacylevel_permission_me(
assert check is expected assert check is expected
# "me" expects true since the object can be private but share with followers
@pytest.mark.parametrize( @pytest.mark.parametrize(
"privacy_level,expected", "privacy_level,expected",
[("me", False), ("followers", True), ("instance", False), ("everyone", True)], [("me", True), ("followers", True), ("instance", False), ("everyone", True)],
) )
def test_privacylevel_permission_followers( def test_privacylevel_permission_followers(
factories, api_request, anonymous_user, privacy_level, expected, mocker factories, api_request, anonymous_user, privacy_level, expected, mocker
......
...@@ -195,6 +195,9 @@ def test_attachment_serializer_existing_file(factories, to_api_date): ...@@ -195,6 +195,9 @@ def test_attachment_serializer_existing_file(factories, to_api_date):
"urls": { "urls": {
"source": attachment.url, "source": attachment.url,
"original": federation_utils.full_url(attachment.file.url), "original": federation_utils.full_url(attachment.file.url),
"small_square_crop": federation_utils.full_url(
attachment.file.crop["50x50"].url
),
"medium_square_crop": federation_utils.full_url( "medium_square_crop": federation_utils.full_url(
attachment.file.crop["200x200"].url attachment.file.crop["200x200"].url
), ),
...@@ -225,6 +228,9 @@ def test_attachment_serializer_remote_file(factories, to_api_date): ...@@ -225,6 +228,9 @@ def test_attachment_serializer_remote_file(factories, to_api_date):
"urls": { "urls": {
"source": attachment.url, "source": attachment.url,
"original": federation_utils.full_url(proxy_url + "?next=original"), "original": federation_utils.full_url(proxy_url + "?next=original"),
"small_square_crop": federation_utils.full_url(
proxy_url + "?next=small_square_crop"
),
"medium_square_crop": federation_utils.full_url( "medium_square_crop": federation_utils.full_url(
proxy_url + "?next=medium_square_crop" proxy_url + "?next=medium_square_crop"
), ),
......
import logging
import pytest
from funkwhale_api.contrib.archivedl import tasks
def test_check_existing_download_task(factories, caplog, mocker):
logger = logging.getLogger("funkwhale_api.contrib.archivedl")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
upload = factories["music.Upload"](
third_party_provider="archive-dl", import_status="pending"
)
mocker.patch("funkwhale_api.contrib.archivedl.tasks.fetch_json", return_value={})
tasks.archive_download(track_id=upload.track.id, conf={})
assert (
"Upload for this track already exist or is pending. Stopping task"
in caplog.text
)
def test_check_last_third_party_queries(factories, caplog, mocker):
logger = logging.getLogger("funkwhale_api.contrib.archivedl")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
factories["music.Upload"].create_batch(
size=10, third_party_provider="archive-dl", import_status="pending"
)
track = factories["music.Track"]()
mocker.patch("funkwhale_api.contrib.archivedl.tasks.fetch_json", return_value={})
with pytest.raises(KeyError):
tasks.archive_download(track_id=track.id, conf={})
assert (
"Last archive.org query was too recent. Trying to wait 2 seconds..."
in caplog.text
)
import pytest import pytest
from funkwhale_api.audio.serializers import ChannelSerializer
from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.federation import api_serializers, serializers from funkwhale_api.federation import api_serializers, serializers
from funkwhale_api.users import serializers as users_serializers from funkwhale_api.users import serializers as users_serializers
...@@ -128,6 +129,7 @@ def test_fetch_serializer_no_obj(factories, to_api_date): ...@@ -128,6 +129,7 @@ def test_fetch_serializer_no_obj(factories, to_api_date):
"status": fetch.status, "status": fetch.status,
"detail": fetch.detail, "detail": fetch.detail,
"object": None, "object": None,
"type": None,
"actor": serializers.APIActorSerializer(fetch.actor).data, "actor": serializers.APIActorSerializer(fetch.actor).data,
} }
...@@ -135,22 +137,28 @@ def test_fetch_serializer_no_obj(factories, to_api_date): ...@@ -135,22 +137,28 @@ def test_fetch_serializer_no_obj(factories, to_api_date):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"object_factory, expected_type, expected_id", "object_factory, expected_type, serializer_class",
[ [
("music.Album", "album", "id"), ("music.Album", "album", serializers.AlbumSerializer),
("music.Artist", "artist", "id"), ("music.Artist", "artist", serializers.ArtistSerializer),
("music.Track", "track", "id"), ("music.Track", "track", serializers.TrackSerializer),
("music.Library", "library", "uuid"), ("audio.Channel", "channel", ChannelSerializer),
("music.Upload", "upload", "uuid"), ("federation.Actor", "account", serializers.APIActorSerializer),
("audio.Channel", "channel", "uuid"), ("playlists.Playlist", "playlist", serializers.PlaylistSerializer),
("federation.Actor", "account", "full_username"),
], ],
) )
def test_fetch_serializer_with_object( def test_fetch_serializer_with_object(
object_factory, expected_type, expected_id, factories, to_api_date object_factory, expected_type, serializer_class, factories, to_api_date
): ):
obj = factories[object_factory]() obj = factories[object_factory]()
fetch = factories["federation.Fetch"](object=obj) fetch = factories["federation.Fetch"](object=obj)
# Serialize the object
if serializer_class:
object_data = serializer_class(obj).data
else:
object_data = {"uuid": getattr(obj, "uuid", None)}
expected = { expected = {
"id": fetch.pk, "id": fetch.pk,
"url": fetch.url, "url": fetch.url,
...@@ -158,7 +166,10 @@ def test_fetch_serializer_with_object( ...@@ -158,7 +166,10 @@ def test_fetch_serializer_with_object(
"fetch_date": None, "fetch_date": None,
"status": fetch.status, "status": fetch.status,
"detail": fetch.detail, "detail": fetch.detail,
"object": {"type": expected_type, expected_id: getattr(obj, expected_id)}, "object": {
**object_data,
},
"type": expected_type,
"actor": serializers.APIActorSerializer(fetch.actor).data, "actor": serializers.APIActorSerializer(fetch.actor).data,
} }
...@@ -175,6 +186,7 @@ def test_fetch_serializer_unhandled_obj(factories, to_api_date): ...@@ -175,6 +186,7 @@ def test_fetch_serializer_unhandled_obj(factories, to_api_date):
"status": fetch.status, "status": fetch.status,
"detail": fetch.detail, "detail": fetch.detail,
"object": None, "object": None,
"type": None,
"actor": serializers.APIActorSerializer(fetch.actor).data, "actor": serializers.APIActorSerializer(fetch.actor).data,
} }
......
...@@ -35,6 +35,29 @@ def test_user_can_fetch_library_using_url(mocker, factories, logged_in_api_clien ...@@ -35,6 +35,29 @@ def test_user_can_fetch_library_using_url(mocker, factories, logged_in_api_clien
assert response.data["results"] == [api_serializers.LibrarySerializer(library).data] assert response.data["results"] == [api_serializers.LibrarySerializer(library).data]
def test_user_can_fetch_playlist_library_using_url(
mocker, factories, logged_in_api_client
):
pl_library = factories["music.Library"]()
upload = factories["music.Upload"]()
upload.playlist_libraries.add(pl_library)
mocked_retrieve = mocker.patch(
"funkwhale_api.federation.utils.retrieve_ap_object", return_value=pl_library
)
url = reverse("api:v1:federation:libraries-fetch")
response = logged_in_api_client.post(url, {"fid": pl_library.fid})
assert mocked_retrieve.call_count == 1
args = mocked_retrieve.call_args
assert args[0] == (pl_library.fid,)
assert args[1]["queryset"].model == views.MusicLibraryViewSet.queryset.model
assert args[1]["serializer_class"] == serializers.LibrarySerializer
assert response.status_code == 200
assert response.data["results"] == [
api_serializers.LibrarySerializer(pl_library).data
]
def test_user_can_schedule_library_scan(mocker, factories, logged_in_api_client): def test_user_can_schedule_library_scan(mocker, factories, logged_in_api_client):
actor = logged_in_api_client.user.create_actor() actor = logged_in_api_client.user.create_actor()
library = factories["music.Library"](privacy_level="everyone") library = factories["music.Library"](privacy_level="everyone")
...@@ -243,7 +266,7 @@ def test_can_fetch_using_url_synchronous( ...@@ -243,7 +266,7 @@ def test_can_fetch_using_url_synchronous(
fetch_task = mocker.patch.object(tasks, "fetch", side_effect=fake_task) fetch_task = mocker.patch.object(tasks, "fetch", side_effect=fake_task)
url = reverse("api:v1:federation:fetches-list") url = reverse("api:v1:federation:fetches-list")
data = {"object": object_id} data = {"object_uri": object_id}
response = logged_in_api_client.post(url, data) response = logged_in_api_client.post(url, data)
assert response.status_code == 201 assert response.status_code == 201
...@@ -266,7 +289,7 @@ def test_fetch_duplicate(factories, logged_in_api_client, settings, now): ...@@ -266,7 +289,7 @@ def test_fetch_duplicate(factories, logged_in_api_client, settings, now):
creation_date=now - datetime.timedelta(seconds=59), creation_date=now - datetime.timedelta(seconds=59),
) )
url = reverse("api:v1:federation:fetches-list") url = reverse("api:v1:federation:fetches-list")
data = {"object": object_id} data = {"object_uri": object_id}
response = logged_in_api_client.post(url, data) response = logged_in_api_client.post(url, data)
assert response.status_code == 201 assert response.status_code == 201
assert response.data == api_serializers.FetchSerializer(duplicate).data assert response.data == api_serializers.FetchSerializer(duplicate).data
...@@ -286,7 +309,7 @@ def test_fetch_duplicate_bypass_with_force( ...@@ -286,7 +309,7 @@ def test_fetch_duplicate_bypass_with_force(
creation_date=now - datetime.timedelta(seconds=59), creation_date=now - datetime.timedelta(seconds=59),
) )
url = reverse("api:v1:federation:fetches-list") url = reverse("api:v1:federation:fetches-list")
data = {"object": object_id, "force": True} data = {"object_uri": object_id, "force": True}
response = logged_in_api_client.post(url, data) response = logged_in_api_client.post(url, data)
fetch = actor.fetches.latest("id") fetch = actor.fetches.latest("id")
......
...@@ -1268,6 +1268,7 @@ def test_inbox_update_playlist(factories, mocker): ...@@ -1268,6 +1268,7 @@ def test_inbox_update_playlist(factories, mocker):
playlist_data = serializers.PlaylistSerializer(playlist_updated).data playlist_data = serializers.PlaylistSerializer(playlist_updated).data
playlist_data["id"] = str(playlist.fid) playlist_data["id"] = str(playlist.fid)
playlist_updated.delete()
routes.inbox_update_playlist( routes.inbox_update_playlist(
{"object": playlist_data}, {"object": playlist_data},
......
This diff is collapsed.
This diff is collapsed.
File added
This diff is collapsed.