From 902243b96909e986087a80e4d382f5c2034ada2a Mon Sep 17 00:00:00 2001 From: Mark Hoekveen Date: Sat, 5 Jul 2025 20:37:41 +0200 Subject: [PATCH 1/4] adds openapi definitions to backend --- backend/muzak/settings.py | 10 +++++++++- backend/playlist/urls.py | 3 +++ backend/requirements.txt | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/muzak/settings.py b/backend/muzak/settings.py index 20cd059..8470425 100644 --- a/backend/muzak/settings.py +++ b/backend/muzak/settings.py @@ -29,6 +29,7 @@ INSTALLED_APPS = [ 'django_htmx', 'rest_framework', 'corsheaders', + 'drf_spectacular', ] MIDDLEWARE = [ @@ -97,7 +98,14 @@ REST_FRAMEWORK = { ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', - ] + ], + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'HDcon Muzak', + 'DESCRIPTION': 'Gewoon een leuk tooltje om samen met je vrienden een muzieklijst samen te stellen.', + 'VERSION': '1.0.0', } OIDC_RP_CLIENT_ID = os.getenv('OIDC_RP_CLIENT_ID') diff --git a/backend/playlist/urls.py b/backend/playlist/urls.py index 5f670b8..bda1144 100644 --- a/backend/playlist/urls.py +++ b/backend/playlist/urls.py @@ -2,10 +2,13 @@ from django.urls import path from . import views from django.conf import settings from django.conf.urls.static import static +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ ### API path('api/', views.api_root, name="api-root"), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/track/', views.TrackListView.as_view(), name="api-tracks"), path('api/track/', views.TrackDetailView.as_view(), name="api-track"), path('api/artist/', views.ArtistDetailView.as_view(), name="api-artist"), diff --git a/backend/requirements.txt b/backend/requirements.txt index 33e9586..6b3e994 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,4 @@ djangorestframework markdown PyYAML Pygments +drf-spectacular From 7a282e23f5a8f060a7ba4b5ab1f6d19aaa463219 Mon Sep 17 00:00:00 2001 From: Mark Hoekveen Date: Sun, 13 Jul 2025 09:45:19 +0200 Subject: [PATCH 2/4] openapi definitions for track --- backend/playlist/serializers/track.py | 45 +++++++++++++++++++++++++++ backend/playlist/views/api/track.py | 32 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/backend/playlist/serializers/track.py b/backend/playlist/serializers/track.py index 3fe7182..a985026 100644 --- a/backend/playlist/serializers/track.py +++ b/backend/playlist/serializers/track.py @@ -1,6 +1,51 @@ from playlist.models import Track from rest_framework import serializers +from drf_spectacular.utils import extend_schema_serializer, OpenApiExample + +@extend_schema_serializer( + examples = [ + OpenApiExample( + 'Song with banter', + value={ + "url": "http://127.0.0.1:8001/api/track/0AzD1FEuvkXP1verWfaZdv", + "name": "Cbat", + "explicit": False, + "duration_ms": 171720, + "popularity": 0, + "banter": "Als je denkt dat \"Cbat\" van Hudson Mohawke de soundtrack is voor het onhandig manoeuvreren van een stelletje klunzen die elkaar constant in de weg zitten, dan heb je waarschijnlijk gelijk. Dit nummer, waarin de beats net zo chaotisch zijn als de liefdesperikelen van een tiener op een eerste date, is het perfecte liedje voor iedereen die zich ooit heeft afgevraagd hoe je een kat kunt imiteren zonder dat je daadwerkelijk een kat bent.", + "banter_done": True, + "old": False, + "from_playlist": None, + "nominated_by": "http://127.0.0.1:8001/api/user/1", + "album": "http://127.0.0.1:8001/api/album/0d99LxnQpiPLgSGDRuU9HT", + "artists": [ + "http://127.0.0.1:8001/api/artist/6olWbKW2VLhFCHfOi0iEDb" + ] + } + ), + OpenApiExample( + 'Song from old playlist', + description='While this track is from an old playlist, old is still set to False as it has been nominated again this year.', + value={ + "url": "http://127.0.0.1:8001/api/track/4QGUlo1swUpXduW23JM57S", + "name": "You Bring On The Sun", + "explicit": False, + "duration_ms": 215400, + "popularity": 43, + "banter": None, + "banter_done": False, + "old": False, + "from_playlist": "HD Con 2020 A", + "nominated_by": None, + "album": "http://127.0.0.1:8001/api/album/01QglfxMtbN2EVEyGWpPyQ", + "artists": [ + "http://127.0.0.1:8001/api/artist/0gcMPgunYh4rX1UOdvZKBn" + ] + } + ), + ] +) class TrackSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Track diff --git a/backend/playlist/views/api/track.py b/backend/playlist/views/api/track.py index 3249bf8..b3c11fd 100644 --- a/backend/playlist/views/api/track.py +++ b/backend/playlist/views/api/track.py @@ -4,13 +4,23 @@ from playlist.models import Track, Profile from playlist.serializers import TrackSerializer from playlist.spotify import spt from playlist.tasks import get_banter, get_and_dither_image +from drf_spectacular.utils import extend_schema class TrackListView(APIView): + @extend_schema( + responses={200: TrackSerializer(many=True)}, + description="Retrieve a list of all tracks." + ) def get(self, request): tracks = Track.objects.all() serializer = TrackSerializer(tracks, many=True, context={'request': request}) return Response(serializer.data) + @extend_schema( + request=None, + responses={201: TrackSerializer, 403: {'description': 'Forbidden'}}, + description="Create a new track from a Spotify link." + ) def post(self, request): (profile, _) = Profile.objects.get_or_create(user=request.user) (can_nom, reason) = profile.can_nominate @@ -20,6 +30,11 @@ class TrackListView(APIView): serializer = TrackSerializer(track, context={'request': request}) return Response(serializer.data) + @extend_schema( + request=None, + responses={204: None, 403: {'description': 'Forbidden'}}, + description="Delete all tracks. Only superusers are allowed." + ) def delete(self, request): if request.user.is_superuser: Track.objects.all().delete() @@ -28,7 +43,24 @@ class TrackListView(APIView): return Response({'error': 'Only superusers can delete all tracks'}, status=403) class TrackDetailView(APIView): + @extend_schema( + responses={200: TrackSerializer}, + description="Retrieve a track by its ID." + ) def get(self, request, id): track = Track.objects.get(pk=id) serializer = TrackSerializer(track, context={'request': request}) return Response(serializer.data) + + @extend_schema( + request=None, + responses={204: None, 403: {'description': 'Forbidden'}}, + description="Delete a single track by its ID. Only superusers are allowed." + ) + def delete(self, request, id): + if request.user.is_superuser: + track = Track.objects.get(pk=id) + track.delete() + return Response(status=204) + else: + return Response({'error': 'Only superusers can delete tracks'}, status=403) From 43c6d7168b77ed1f8d0837e0a648aa1a0094a8cc Mon Sep 17 00:00:00 2001 From: Mark Hoekveen Date: Sun, 13 Jul 2025 11:31:11 +0200 Subject: [PATCH 3/4] openapi defs for album and artist --- backend/playlist/serializers/album.py | 35 ++++++++++++++++++++++++++ backend/playlist/serializers/artist.py | 21 ++++++++++++++++ backend/playlist/views/api/album.py | 3 +++ backend/playlist/views/api/artist.py | 3 +++ 4 files changed, 62 insertions(+) diff --git a/backend/playlist/serializers/album.py b/backend/playlist/serializers/album.py index 43c2413..abd0b38 100644 --- a/backend/playlist/serializers/album.py +++ b/backend/playlist/serializers/album.py @@ -1,7 +1,42 @@ +from os import extsep from django.db.models.functions import Extract from playlist.models import Album from rest_framework import serializers +from drf_spectacular.utils import extend_schema_serializer, OpenApiExample +@extend_schema_serializer( + examples = [ + OpenApiExample( + name='Album Example', + value = { + "url": "http://muzak.hoekveen.net/api/album/7MejfRSNnrpcLZIxkeZDqR", + "name": "Leftoverture (Expanded Edition)", + "album_type": "album", + "total_tracks": 10, + "image": "http://127.0.0.1:8001/albums/Leftoverture_Expanded_Edition.png", + "image_url": "https://i.scdn.co/image/ab67616d0000b2731be40e44db112e123e5e8b51", + "artists": [ + "http://127.0.0.1:8001/api/artist/2hl0xAkS2AIRAu23TVMBG1" + ] + } + ), + OpenApiExample( + name = 'Multiple artist single', + value = { + "url": "http://127.0.0.1:8001/api/album/1pHD8AFu4z1CvuTPjZFOFi", + "name": "ラビリンス", + "album_type": "single", + "total_tracks": 4, + "image": None, + "image_url": "https://i.scdn.co/image/ab67616d0000b2731607d3aa3a69ca0e1ffbe26b", + "artists": [ + "http://127.0.0.1:8001/api/artist/4ZX8Wr8KHHrW7radu6IwYG", + "http://127.0.0.1:8001/api/artist/4d2zOuYJHBPJTpVblHEKJb" + ] + } + ) + ] +) class AlbumSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Album diff --git a/backend/playlist/serializers/artist.py b/backend/playlist/serializers/artist.py index 6742b58..53d3fbc 100644 --- a/backend/playlist/serializers/artist.py +++ b/backend/playlist/serializers/artist.py @@ -1,6 +1,27 @@ from playlist.models import Artist from rest_framework import serializers +from drf_spectacular.utils import extend_schema_serializer, OpenApiExample +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'Daft Punk', + value = { + "id": "4tZwfgrHOc3mvqYlEYSvVi", + "name": "Daft Punk", + "url": "http://127.0.0.1:8001/api/artist/4tZwfgrHOc3mvqYlEYSvVi" + } + ), + OpenApiExample( + 'コリッキー', + value = { + "id": "307y5sbPNvRpXjBcZgS25q", + "name": "コリッキー", + "url": "http://127.0.0.1:8001/api/artist/307y5sbPNvRpXjBcZgS25q" + } + ) + ] +) class ArtistSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Artist diff --git a/backend/playlist/views/api/album.py b/backend/playlist/views/api/album.py index 5c9ba25..1345087 100644 --- a/backend/playlist/views/api/album.py +++ b/backend/playlist/views/api/album.py @@ -2,14 +2,17 @@ from rest_framework.views import APIView from rest_framework.response import Response from playlist.models import Album from playlist.serializers import AlbumSerializer +from drf_spectacular.utils import extend_schema class AlbumListView(APIView): + @extend_schema(responses={200: AlbumSerializer(many=True)}) def get(self, request): albums = Album.objects.all() serializer = AlbumSerializer(albums, many=True, context={'request': request}) return Response(serializer.data) class AlbumDetailView(APIView): + @extend_schema(responses={200: AlbumSerializer}) def get(self, request, id): album = Album.objects.get(pk=id) serializer = AlbumSerializer(album, context={'request': request}) diff --git a/backend/playlist/views/api/artist.py b/backend/playlist/views/api/artist.py index 89185df..ccb03e5 100644 --- a/backend/playlist/views/api/artist.py +++ b/backend/playlist/views/api/artist.py @@ -3,13 +3,16 @@ from rest_framework.views import APIView from rest_framework.response import Response from playlist.models import Artist from playlist.serializers import ArtistSerializer +from drf_spectacular.utils import extend_schema +@extend_schema(responses={200: ArtistSerializer(many=True)}) class ArtistListView(APIView): def get(self, request): artists = Artist.objects.all() serializer = ArtistSerializer(artists, many=True, context={'request': request}) return Response(serializer.data) +@extend_schema(responses={200: ArtistSerializer}) class ArtistDetailView(APIView): def get(self, request, id): artist = Artist.objects.get(pk=id) From 6f867415f81228be4f4c3fe1f5a71d5ac3327295 Mon Sep 17 00:00:00 2001 From: Mark Hoekveen Date: Sun, 13 Jul 2025 12:56:22 +0200 Subject: [PATCH 4/4] adds new config settings to example env --- backend/example.env | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/example.env b/backend/example.env index ea4896b..d2236e5 100644 --- a/backend/example.env +++ b/backend/example.env @@ -1,11 +1,13 @@ SPOTIFY_ENCODED_ID= OIDC_RP_CLIENT_ID= OIDC_RP_CLIENT_SECRET= +OIDC_RP_SIGN_ALGO=RS256 OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.yourwebsite.com/application/o/authorize/ OIDC_OP_TOKEN_ENDPOINT=https://auth.yourwebsite.com/application/o/token/ OIDC_OP_USER_ENDPOINT=https://auth.yourwebsite.com/application/o/userinfo/ +OIDC_OP_JWKS_ENDPOINT=https://auth.yourwebsite.com/application/o/name/jwks/ AI_ENDPOINT=http://localhost:11434/api/generate AI_MODEL=myModel DATE_OPEN= DATE_NOM_END= -DATE_VOTE_END= \ No newline at end of file +DATE_VOTE_END=