Data Access Layer #13

Merged
guus merged 4 commits from add-data-layer into master 2025-07-28 20:11:09 +02:00
7 changed files with 81 additions and 22 deletions
Showing only changes of commit 31c6fd3265 - Show all commits
+5
View File
@@ -104,6 +104,11 @@ class Track(models.Model):
artists = list(map(lambda a:a.name, self.artists.all())) artists = list(map(lambda a:a.name, self.artists.all()))
return " & ".join(artists) return " & ".join(artists)
@property
def album_cover(self):
if self.album:
return self.album.image_url
@property @property
def name_sanitized(self): def name_sanitized(self):
return re.match("[^\-\(]*\w", self.name)[0] return re.match("[^\-\(]*\w", self.name)[0]
+4 -1
View File
@@ -47,6 +47,9 @@ from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
] ]
) )
class TrackSerializer(serializers.HyperlinkedModelSerializer): class TrackSerializer(serializers.HyperlinkedModelSerializer):
artist = serializers.CharField()
album_cover = serializers.CharField()
class Meta: class Meta:
model = Track model = Track
fields = '__all__' fields = '__all__'
@@ -54,5 +57,5 @@ class TrackSerializer(serializers.HyperlinkedModelSerializer):
'url': {'view_name': 'api-track', 'lookup_field': 'id'}, 'url': {'view_name': 'api-track', 'lookup_field': 'id'},
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'}, 'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
'album': {'view_name': 'api-album', 'lookup_field': 'id'}, 'album': {'view_name': 'api-album', 'lookup_field': 'id'},
'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'} 'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'},
} }
+1
View File
@@ -10,6 +10,7 @@ urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 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.TrackListView.as_view(), name="api-tracks"),
path('api/track/vote', views.TrackVoteView.as_view(), name="api-track-vote"),
path('api/track/<id>', views.TrackDetailView.as_view(), name="api-track"), path('api/track/<id>', views.TrackDetailView.as_view(), name="api-track"),
path('api/artist/<id>', views.ArtistDetailView.as_view(), name="api-artist"), path('api/artist/<id>', views.ArtistDetailView.as_view(), name="api-artist"),
path('api/artist', views.ArtistListView.as_view(), name="api-artists"), path('api/artist', views.ArtistListView.as_view(), name="api-artists"),
+14
View File
@@ -5,6 +5,7 @@ from playlist.serializers import TrackSerializer
from playlist.spotify import spt from playlist.spotify import spt
from playlist.tasks import get_banter, get_and_dither_image from playlist.tasks import get_banter, get_and_dither_image
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from random import choice
class TrackListView(APIView): class TrackListView(APIView):
@extend_schema( @extend_schema(
@@ -64,3 +65,16 @@ class TrackDetailView(APIView):
return Response(status=204) return Response(status=204)
else: else:
return Response({'error': 'Only superusers can delete tracks'}, status=403) return Response({'error': 'Only superusers can delete tracks'}, status=403)
class TrackVoteView(APIView):
@extend_schema(
request=None,
responses={200: TrackSerializer},
description="Retrieve a random track for voting."
)
def get(self, request):
# TODO implement a session-based vote tracking system. Random return for now.
random_pk = choice(Track.objects.values_list('pk', flat=True))
track = Track.objects.get(pk=random_pk)
serializer = TrackSerializer(track, context={'request': request})
return Response(serializer.data)
+7
View File
@@ -0,0 +1,7 @@
const nextConfig = {
images: {
remotePatterns: [new URL("https://i.scdn.co/**")],
},
};
export default nextConfig;
+49 -21
View File
@@ -4,6 +4,7 @@ import { Nunito } from "next/font/google";
import users from "muzak/data/users.json"; import users from "muzak/data/users.json";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { api, Track } from "muzak/data/fetcher";
const nunito = Nunito({ const nunito = Nunito({
weight: "900", weight: "900",
@@ -16,15 +17,25 @@ export default function Lobby() {
const [yesVoteList, setYesVoteList] = useState([]); const [yesVoteList, setYesVoteList] = useState([]);
const [noVoteList, setNoVoteList] = useState([]); const [noVoteList, setNoVoteList] = useState([]);
const [currentSong, setCurrentSong] = useState({ const [currentSong, setCurrentSong] = useState<Track>();
artist: "The Meowls", const [songLoading, setSongLoading] = useState(false);
title: "The Owls Are Not What They Seem",
});
useEffect(() => { useEffect(() => {
populateUserList(); populateUserList();
}, [partySize]); }, [partySize]);
async function getTrack() {
setSongLoading(true);
const { data: track } = await api.tracks.vote({});
setCurrentSong(track);
console.log("I got a track!", track.name);
setSongLoading(false);
}
useEffect(() => {
if (!currentSong && !songLoading) getTrack();
}, []);
function populateUserList() { function populateUserList() {
const mappedUsers = users.slice(0, partySize).map((user) => ({ const mappedUsers = users.slice(0, partySize).map((user) => ({
id: user.id, id: user.id,
@@ -50,7 +61,7 @@ export default function Lobby() {
<div className="flex flex-1 items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="flex h-[170px] w-[170px] items-center justify-center rounded-full border-2 bg-orange-400"> <div className="flex h-[170px] w-[170px] items-center justify-center rounded-full border-2 bg-orange-400">
<div className="text-5xl text-white drop-shadow-sm drop-shadow-black"> <div className="text-5xl text-white drop-shadow-sm drop-shadow-black">
30 <button onClick={getTrack}>30</button>
</div> </div>
</div> </div>
</div> </div>
@@ -86,10 +97,25 @@ export default function Lobby() {
</div> </div>
))}{" "} ))}{" "}
</div> </div>
{!songLoading && currentSong ? (
<div className="rounded-md bg-black p-2"> <div className="rounded-md bg-black p-2">
<Image src="/meowl.png" alt="meowl" width={500} height={500} /> <Image
</div> src={currentSong.album_cover}
alt={currentSong.name}
width={500}
height={500}
/>
</div>
) : (
<div className="rounded-md bg-black p-2">
<Image
src="/meowl.png"
alt="Loading.."
width={500}
height={500}
/>
</div>
)}
<div className="grid min-w-52 grid-flow-row grid-cols-3 items-start justify-start gap-4 rounded bg-blue-950/80 p-4"> <div className="grid min-w-52 grid-flow-row grid-cols-3 items-start justify-start gap-4 rounded bg-blue-950/80 p-4">
{userList.map((user) => ( {userList.map((user) => (
<div <div
@@ -104,18 +130,20 @@ export default function Lobby() {
))}{" "} ))}{" "}
</div> </div>
</div> </div>
<div className="flex flex-col items-center justify-start"> {!songLoading && currentSong && (
<h2 <div className="flex flex-col items-center justify-start">
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`} <h2
> className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
{currentSong.artist} >
</h2> {currentSong.artist}
<h2 </h2>
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`} <h2
> className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
{currentSong.title} >
</h2> {currentSong.name}
</div> </h2>
</div>
)}
</div> </div>
<div className="flex flex-row items-start justify-start gap-10 rounded bg-blue-950/80 p-4"> <div className="flex flex-row items-start justify-start gap-10 rounded bg-blue-950/80 p-4">
+1
View File
@@ -49,6 +49,7 @@ export const api = {
tracks: { tracks: {
list: fetcher.path("/api/track/").method("get").create(), list: fetcher.path("/api/track/").method("get").create(),
detail: fetcher.path("/api/track/{id}").method("get").create(), detail: fetcher.path("/api/track/{id}").method("get").create(),
vote: fetcher.path("/api/track/vote").method("get").create(),
}, },
artists: { artists: {
list: fetcher.path("/api/artist").method("get").create(), list: fetcher.path("/api/artist").method("get").create(),