Feat(API): changes to support new frontend

Co-Authored-By: ArneBo <arne@ecobasa.org>
Co-Authored-By: Flupsi <upsiflu@gmail.com>
Co-Authored-By: jon r <jon@allmende.io>
This commit is contained in:
jon r 2025-04-18 10:37:04 +02:00
parent 06f0a8f1e3
commit 90b853b722
18 changed files with 351 additions and 18 deletions

View File

@ -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"),
],

View File

@ -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()

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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,
)

View File

@ -49,6 +49,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
"duration",
"is_playable",
"actor",
"description",
)
read_only_fields = ["id", "modification_date", "creation_date"]

View File

@ -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
)

View File

@ -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

View File

@ -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"
),

View File

@ -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

View File

@ -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)

View File

@ -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
),