diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 776d73c2e..e8647d9c1 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -1398,6 +1398,7 @@ VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = { ], "attachment_square": [ ("original", "url"), + ("small_square_crop", "crop__50x50"), ("medium_square_crop", "crop__200x200"), ("large_square_crop", "crop__600x600"), ], diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py index 7d6b1566d..b0bd1a46d 100644 --- a/api/funkwhale_api/audio/serializers.py +++ b/api/funkwhale_api/audio/serializers.py @@ -263,6 +263,7 @@ class ChannelSerializer(serializers.ModelSerializer): attributed_to = federation_serializers.APIActorSerializer() rss_url = serializers.CharField(source="get_rss_url") url = serializers.SerializerMethodField() + subscriptions_count = serializers.SerializerMethodField() class Meta: model = models.Channel @@ -276,6 +277,7 @@ class ChannelSerializer(serializers.ModelSerializer): "rss_url", "url", "downloads_count", + "subscriptions_count", ] def to_representation(self, obj): @@ -284,6 +286,7 @@ class ChannelSerializer(serializers.ModelSerializer): data["subscriptions_count"] = self.get_subscriptions_count(obj) return data + @extend_schema_field(OpenApiTypes.INT) def get_subscriptions_count(self, obj) -> int: return obj.actor.received_follows.exclude(approved=False).count() diff --git a/api/funkwhale_api/audio/views.py b/api/funkwhale_api/audio/views.py index 0fc4d7de4..e30143ed1 100644 --- a/api/funkwhale_api/audio/views.py +++ b/api/funkwhale_api/audio/views.py @@ -2,10 +2,12 @@ from django import http from django.db import transaction from django.db.models import Count, Prefetch, Q, Sum from django.utils import timezone -from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.utils import extend_schema, extend_schema_view, inline_serializer from rest_framework import decorators, exceptions, mixins from rest_framework import permissions as rest_permissions -from rest_framework import response, viewsets +from rest_framework import response +from rest_framework import serializers as rest_serializers +from rest_framework import viewsets from funkwhale_api.common import locales, permissions, preferences from funkwhale_api.common import utils as common_utils @@ -210,6 +212,32 @@ class ChannelViewSet( data = serializers.rss_serialize_channel_full(channel=object, uploads=uploads) return response.Response(data, status=200) + @extend_schema( + responses=inline_serializer( + name="MetedataChoicesSerializer", + fields={ + "language": rest_serializers.ListField( + child=inline_serializer( + name="LanguageItem", + fields={ + "value": rest_serializers.CharField(), + "label": rest_serializers.CharField(), + }, + ) + ), + "itunes_category": rest_serializers.ListField( + child=inline_serializer( + name="iTunesCategoryItem", + fields={ + "value": rest_serializers.CharField(), + "label": rest_serializers.CharField(), + "children": rest_serializers.CharField(), + }, + ) + ), + }, + ) + ) @decorators.action( methods=["get"], detail=False, diff --git a/api/funkwhale_api/common/models.py b/api/funkwhale_api/common/models.py index 575ff864b..f6e2c1213 100644 --- a/api/funkwhale_api/common/models.py +++ b/api/funkwhale_api/common/models.py @@ -257,6 +257,13 @@ class Attachment(models.Model): proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": self.uuid}) return federation_utils.full_url(proxy_url + "?next=original") + @property + def download_url_small_square_crop(self): + if self.file: + return utils.media_url(self.file.crop["50x50"].url) + proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": self.uuid}) + return federation_utils.full_url(proxy_url + "?next=small_square_crop") + @property def download_url_medium_square_crop(self): if self.file: diff --git a/api/funkwhale_api/common/schema.yml b/api/funkwhale_api/common/schema.yml index 589a05aea..bfafcd68c 100644 --- a/api/funkwhale_api/common/schema.yml +++ b/api/funkwhale_api/common/schema.yml @@ -2152,7 +2152,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Channel' + $ref: '#/components/schemas/MetedataChoices' description: '' /api/v1/channels/rss-subscribe/: post: @@ -9291,16 +9291,25 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' multipart/form-data: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' application/activity+json: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' + required: true security: - oauth2: [] - ApplicationToken: [] @@ -11653,7 +11662,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Channel' + $ref: '#/components/schemas/MetedataChoices' description: '' /api/v2/channels/rss-subscribe/: post: @@ -12927,7 +12936,7 @@ paths: description: '' /api/v2/instance/nodeinfo/2.1/: get: - operationId: getNodeInfo20_2 + operationId: getNodeInfo21 tags: - instance responses: @@ -12935,7 +12944,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NodeInfo20' + $ref: '#/components/schemas/NodeInfo21' description: '' /api/v2/instance/settings/: get: @@ -18957,16 +18966,25 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' multipart/form-data: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' application/activity+json: schema: - $ref: '#/components/schemas/PatchedUploadForOwnerRequest' + type: array + items: + $ref: '#/components/schemas/UploadBulkUpdateRequest' + required: true security: - oauth2: [] - ApplicationToken: [] @@ -20270,12 +20288,16 @@ components: downloads_count: type: integer readOnly: true + subscriptions_count: + type: integer + readOnly: true required: - actor - artist - attributed_to - downloads_count - rss_url + - subscriptions_count - url ChannelCreate: type: object @@ -20926,6 +20948,16 @@ components: required: - channel - uuid + LanguageItem: + type: object + properties: + value: + type: string + label: + type: string + required: + - label + - value Library: type: object properties: @@ -23124,6 +23156,124 @@ components: - shortDescription - supportedUploadExtensions - terms + Metadata21: + type: object + properties: + actorId: + type: string + private: + type: boolean + readOnly: true + shortDescription: + type: string + readOnly: true + longDescription: + type: string + readOnly: true + contactEmail: + type: string + readOnly: true + nodeName: + type: string + readOnly: true + banner: + type: string + readOnly: true + defaultUploadQuota: + type: integer + readOnly: true + supportedUploadExtensions: + type: array + items: + type: string + allowList: + allOf: + - $ref: '#/components/schemas/AllowListStat' + readOnly: true + funkwhaleSupportMessageEnabled: + type: boolean + readOnly: true + instanceSupportMessage: + type: string + readOnly: true + usage: + $ref: '#/components/schemas/MetadataUsage' + languages: + type: array + items: + type: string + location: + type: string + content: + $ref: '#/components/schemas/MetadataContent' + features: + type: array + items: + type: string + codeOfConduct: + type: string + readOnly: true + required: + - actorId + - allowList + - banner + - codeOfConduct + - contactEmail + - content + - defaultUploadQuota + - features + - funkwhaleSupportMessageEnabled + - instanceSupportMessage + - languages + - location + - longDescription + - nodeName + - private + - shortDescription + - supportedUploadExtensions + MetadataContent: + type: object + properties: + local: + $ref: '#/components/schemas/MetadataContentLocal' + topMusicCategories: + type: array + items: + $ref: '#/components/schemas/MetadataContentCategory' + topPodcastCategories: + type: array + items: + $ref: '#/components/schemas/MetadataContentCategory' + required: + - local + - topMusicCategories + - topPodcastCategories + MetadataContentCategory: + type: object + properties: + name: + type: string + count: + type: integer + required: + - count + - name + MetadataContentLocal: + type: object + properties: + artists: + type: integer + releases: + type: integer + recordings: + type: integer + hoursOfContent: + type: integer + required: + - artists + - hoursOfContent + - recordings + - releases MetadataUsage: type: object properties: @@ -23146,6 +23296,20 @@ components: readOnly: true required: - tracks + MetedataChoices: + type: object + properties: + language: + type: array + items: + $ref: '#/components/schemas/LanguageItem' + itunes_category: + type: array + items: + $ref: '#/components/schemas/iTunesCategoryItem' + required: + - itunes_category + - language ModerationTarget: type: object properties: @@ -23248,6 +23412,42 @@ components: - software - usage - version + NodeInfo21: + type: object + properties: + version: + type: string + readOnly: true + software: + $ref: '#/components/schemas/SoftwareSerializer_v2' + protocols: + type: array + items: {} + readOnly: true + services: + allOf: + - $ref: '#/components/schemas/Services' + default: + inbound: [] + outbound: [] + openRegistrations: + type: boolean + readOnly: true + usage: + allOf: + - $ref: '#/components/schemas/Usage' + readOnly: true + metadata: + allOf: + - $ref: '#/components/schemas/Metadata21' + readOnly: true + required: + - metadata + - openRegistrations + - protocols + - software + - usage + - version NodeInfoLibrary: type: object properties: @@ -24425,6 +24625,10 @@ components: maxLength: 100 privacy_level: $ref: '#/components/schemas/PrivacyLevelEnum' + description: + type: string + nullable: true + maxLength: 5000 PatchedRadioRequest: type: object properties: @@ -24455,6 +24659,8 @@ components: allOf: - $ref: '#/components/schemas/ImportStatusEnum' default: pending + privacy_level: + $ref: '#/components/schemas/LibraryPrivacyLevelEnum' import_metadata: {} import_reference: type: string @@ -24546,6 +24752,10 @@ components: is_playable: type: boolean readOnly: true + description: + type: string + nullable: true + maxLength: 5000 required: - actor - album_covers @@ -24576,6 +24786,10 @@ components: maxLength: 100 privacy_level: $ref: '#/components/schemas/PrivacyLevelEnum' + description: + type: string + nullable: true + maxLength: 5000 required: - name PlaylistTrack: @@ -25056,6 +25270,25 @@ components: required: - name - version + SoftwareSerializer_v2: + type: object + properties: + name: + type: string + readOnly: true + version: + type: string + repository: + type: string + readOnly: true + homepage: + type: string + readOnly: true + required: + - homepage + - name + - repository + - version SpaManifest: type: object properties: @@ -25494,6 +25727,17 @@ components: - mimetype - size - uuid + UploadBulkUpdateRequest: + type: object + properties: + uuid: + type: string + format: uuid + privacy_level: + $ref: '#/components/schemas/LibraryPrivacyLevelEnum' + required: + - privacy_level + - uuid UploadForOwner: type: object properties: @@ -25540,6 +25784,8 @@ components: allOf: - $ref: '#/components/schemas/ImportStatusEnum' default: pending + privacy_level: + $ref: '#/components/schemas/LibraryPrivacyLevelEnum' import_details: readOnly: true import_metadata: {} @@ -25580,6 +25826,8 @@ components: allOf: - $ref: '#/components/schemas/ImportStatusEnum' default: pending + privacy_level: + $ref: '#/components/schemas/LibraryPrivacyLevelEnum' import_metadata: {} import_reference: type: string @@ -25845,6 +26093,19 @@ components: minLength: 1 required: - key + iTunesCategoryItem: + type: object + properties: + value: + type: string + label: + type: string + children: + type: string + required: + - children + - label + - value securitySchemes: ApplicationToken: type: http diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index 4461a1c5e..73c294df8 100644 --- a/api/funkwhale_api/common/serializers.py +++ b/api/funkwhale_api/common/serializers.py @@ -308,6 +308,7 @@ class AttachmentSerializer(serializers.Serializer): urls = {} urls["source"] = o.url urls["original"] = o.download_url_original + urls["small_square_crop"] = o.download_url_small_square_crop urls["medium_square_crop"] = o.download_url_medium_square_crop urls["large_square_crop"] = o.download_url_large_square_crop return urls diff --git a/api/funkwhale_api/common/views.py b/api/funkwhale_api/common/views.py index 51c46e2e0..ff3a54e7f 100644 --- a/api/funkwhale_api/common/views.py +++ b/api/funkwhale_api/common/views.py @@ -176,7 +176,12 @@ class AttachmentViewSet( return r size = request.GET.get("next", "original").lower() - if size not in ["original", "medium_square_crop", "large_square_crop"]: + if size not in [ + "original", + "small_square_crop", + "medium_square_crop", + "large_square_crop", + ]: size = "original" try: diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py index f87704452..186b87ccd 100644 --- a/api/funkwhale_api/instance/views.py +++ b/api/funkwhale_api/instance/views.py @@ -126,7 +126,7 @@ class NodeInfo21(NodeInfo20): serializer_class = serializers.NodeInfo21Serializer @extend_schema( - responses=serializers.NodeInfo20Serializer, operation_id="getNodeInfo20" + responses=serializers.NodeInfo21Serializer, operation_id="getNodeInfo21" ) def get(self, request): pref = preferences.all() diff --git a/api/funkwhale_api/music/dynamic_preferences_registry.py b/api/funkwhale_api/music/dynamic_preferences_registry.py index f8e63c72a..1c016a9fd 100644 --- a/api/funkwhale_api/music/dynamic_preferences_registry.py +++ b/api/funkwhale_api/music/dynamic_preferences_registry.py @@ -129,7 +129,7 @@ class Format(types.MultipleChoicePreference): ("aac", "aac"), ("mp3", "mp3"), ] - help_text = "Witch audio format to allow" + help_text = "Which audio format to allow" @global_preferences_registry.register diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index c01585759..4a8ba7705 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -372,6 +372,9 @@ class UploadSerializer(serializers.ModelSerializer): required=False, filters=lambda context: {"actor": context["user"].actor}, ) + privacy_level = serializers.ChoiceField( + choices=models.LIBRARY_PRIVACY_LEVEL_CHOICES, required=False + ) channel = common_serializers.RelatedField( "uuid", ChannelSerializer(), @@ -395,6 +398,7 @@ class UploadSerializer(serializers.ModelSerializer): "size", "import_date", "import_status", + "privacy_level", ] read_only_fields = [ @@ -495,6 +499,7 @@ class UploadForOwnerSerializer(UploadSerializer): r = super().to_representation(obj) if "audio_file" in r: del r["audio_file"] + r["privacy_level"] = obj.library.privacy_level return r def validate(self, validated_data): diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 0e9420f9c..ba9336037 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -798,6 +798,9 @@ class UploadViewSet( cover_data["content"] = base64.b64encode(cover_data["content"]) return Response(payload, status=200) + @extend_schema( + request=serializers.UploadBulkUpdateSerializer(many=True), + ) @action(detail=False, methods=["patch"]) def bulk_update(self, request, *args, **kwargs): """ @@ -811,7 +814,9 @@ class UploadViewSet( models.Upload.objects.bulk_update(serializer.validated_data, ["library"]) return Response( - serializers.UploadForOwnerSerializer(serializer.validated_data).data, + serializers.UploadForOwnerSerializer( + serializer.validated_data, many=True + ).data, status=200, ) diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index 7494c2838..b592ef69c 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -49,6 +49,7 @@ class PlaylistSerializer(serializers.ModelSerializer): "duration", "is_playable", "actor", + "description", ) read_only_fields = ["id", "modification_date", "creation_date"] diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index fa4a5a0fe..00910cf85 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -111,6 +111,9 @@ class GetArtistInfo2Serializer(serializers.Serializer): if artist.mbid: payload["musicBrainzId"] = TagValue(artist.mbid) if artist.attachment_cover: + payload["smallImageUrl"] = TagValue( + artist.attachment_cover.download_url_small_square_crop + ) payload["mediumImageUrl"] = TagValue( artist.attachment_cover.download_url_medium_square_crop ) diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py index c48505779..6de46c1ff 100644 --- a/api/tests/audio/test_serializers.py +++ b/api/tests/audio/test_serializers.py @@ -230,6 +230,7 @@ def test_channel_serializer_representation(factories, to_api_date): "rss_url": channel.get_rss_url(), "url": channel.actor.url, "downloads_count": 12, + "subscriptions_count": 0, } expected["artist"]["description"] = common_serializers.ContentSerializer( content @@ -254,6 +255,7 @@ def test_channel_serializer_external_representation(factories, to_api_date): "rss_url": channel.get_rss_url(), "url": channel.actor.url, "downloads_count": 0, + "subscriptions_count": 0, } expected["artist"]["description"] = common_serializers.ContentSerializer( content diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py index 95c0cddc9..73b732041 100644 --- a/api/tests/common/test_serializers.py +++ b/api/tests/common/test_serializers.py @@ -195,6 +195,9 @@ def test_attachment_serializer_existing_file(factories, to_api_date): "urls": { "source": attachment.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( attachment.file.crop["200x200"].url ), @@ -225,6 +228,9 @@ def test_attachment_serializer_remote_file(factories, to_api_date): "urls": { "source": attachment.url, "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( proxy_url + "?next=medium_square_crop" ), diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index c0c7fc100..8f36807d9 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -169,6 +169,7 @@ def test_upload_owner_serializer(factories, to_api_date): "import_details": {"hello": "world"}, "source": "upload://test", "import_reference": "ref", + "privacy_level": upload.library.privacy_level, } serializer = serializers.UploadForOwnerSerializer(upload) assert serializer.data == expected diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py index a79690c6c..e14f4710f 100644 --- a/api/tests/playlists/test_serializers.py +++ b/api/tests/playlists/test_serializers.py @@ -85,6 +85,7 @@ def test_playlist_serializer(factories, to_api_date): "duration": 0, "tracks_count": 0, "album_covers": [], + "description": playlist.description, } serializer = serializers.PlaylistSerializer(playlist) diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index 295345736..1275a678d 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -156,6 +156,9 @@ def test_get_artist_info_2_serializer(factories): expected = { "musicBrainzId": artist.mbid, + "smallImageUrl": renderers.TagValue( + artist.attachment_cover.download_url_small_square_crop + ), "mediumImageUrl": renderers.TagValue( artist.attachment_cover.download_url_medium_square_crop ),