Data Access Layer #13
@@ -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]
|
||||||
|
|||||||
@@ -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'},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [new URL("https://i.scdn.co/**")],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user