116 Commits

Author SHA1 Message Date
mark bbb9d37ec7 adds export script. does some cleanup 2025-08-23 13:50:01 +02:00
guus bcaaae90e9 post-game opleuken 2025-08-22 12:20:11 +02:00
mark b4a037388a adds next track short animation 2025-08-21 20:43:00 +02:00
mark c710845288 post-game update prepare 2025-08-21 20:03:37 +02:00
mark 946548d001 updates post-game time to 12s 2025-08-21 19:54:18 +02:00
mark 7ed8895038 hard next on album click 2025-08-21 19:53:53 +02:00
mark 51697622a7 minimum height on klokje 2025-08-21 19:53:28 +02:00
guus 6b6da07ddf Merge pull request 'localStorage token and websocket reconnect' (#28) from auth-session-updates into master
Reviewed-on: #28
2025-08-21 18:40:06 +02:00
guus 2403734bf3 fix clock position 2025-08-21 18:36:46 +02:00
mark 9184d759f3 localStorage token and websocket reconnect 2025-08-21 09:25:30 +02:00
mark 9c94934ff3 post-game info 2025-08-20 22:48:01 +02:00
guus e72e2e6800 Merge pull request 'auto connects user to session if available' (#26) from auto-session into master
Reviewed-on: #26
2025-08-20 21:55:00 +02:00
guus 65e81a7e6b Merge pull request 'game on' (#27) from game-mode into master
Reviewed-on: #27
2025-08-20 21:54:49 +02:00
mark c23cb63817 game on 2025-08-20 21:51:51 +02:00
mark 5bdf1ed940 auto connects user to session if available 2025-08-20 19:53:32 +02:00
guus 2a23f1dedc changed loading image 2025-08-19 21:02:43 +02:00
guus da10b19960 add gordijntje 2025-08-19 12:54:03 +02:00
guus 5b0ef233c4 update vote boxes 2025-08-19 00:29:17 +02:00
mark 1d50bc3839 updates avatar. bye pravatar 2025-08-17 22:10:41 +02:00
mark 479dbe5c2d remove pause page 2025-08-17 22:07:20 +02:00
mark 4e324502ae cleanup 2025-08-17 21:58:44 +02:00
mark d6cfdf66c6 adds loading text to lobby 2025-08-17 21:56:16 +02:00
mark a64d9fea31 fixes issue with vote not updating 2025-08-17 21:26:10 +02:00
mark b04a4732cf Merge pull request 'spotify player' (#15) from spotify-player into master
Reviewed-on: #15
2025-08-17 20:28:38 +02:00
mark bd71bce450 fixes compilation 2025-08-17 20:28:19 +02:00
mark cfb6b2609b ignores logs 2025-08-17 20:25:47 +02:00
mark 564f2d47b4 spotify player 2025-08-17 20:17:46 +02:00
guus f22ff07577 modify lobby and pause look 2025-08-17 18:54:39 +02:00
mark 47583e36dc vote feedback 2025-08-17 11:47:14 +02:00
mark 7d54f6c5ce fixes userlist 2025-08-17 10:05:51 +02:00
mark 698c41544c process user as object 2025-08-16 23:30:49 +02:00
mark 5de174b162 adds some logging 2025-08-16 23:30:16 +02:00
mark 341f7c8091 move websockets to new path 2025-08-16 15:10:13 +02:00
mark 12344272ae stops sessionchecker from fucking up 2025-08-16 14:34:11 +02:00
mark 762ec15a30 fixes build 2025-08-16 14:11:38 +02:00
mark c7eb174a01 betere sessies lol lmao even 2025-08-16 09:51:29 +02:00
mark a1526ddc67 session link with mobile voting 2025-08-15 18:30:57 +02:00
guus cbc9be1f63 improved pause screen 2025-08-14 21:40:59 +02:00
guus 6216bd2c84 update mobile voting screen 2025-08-14 21:23:05 +02:00
mark 368628c070 session updates. mostly 2025-08-14 15:38:48 +02:00
guus e0a03f00bf responsive mobile-voting btns 2025-08-12 23:27:55 +02:00
guus 76b084b541 add mobile-voting 2025-08-12 15:47:05 +02:00
mark 21d5f56a39 sessions, or something 2025-08-09 16:28:49 +02:00
mark 4c70a3f72d merge fetch and fetcher 2025-08-09 10:46:11 +02:00
mark b20e0c3980 changes login message 2025-08-06 21:00:57 +02:00
mark 8448a865a7 disable nominate when quota 0 2025-08-06 20:57:07 +02:00
guus b45bcf03ab Merge pull request 'final checks' (#14) from mark into master
Reviewed-on: #14
2025-08-03 20:22:15 +02:00
mark 421a004667 small regex update 2025-08-03 20:21:13 +02:00
guus 370507d5a6 add form validation 2025-08-03 20:13:12 +02:00
mark 67d8dac8c0 quota context hook 2025-08-03 20:09:11 +02:00
mark 3eadd5c15d no more update quota 2025-08-03 20:07:49 +02:00
guus 5ea702e7bb fix message styling issue 2025-08-03 19:52:33 +02:00
mark 407abec1bf disables navbar for now 2025-08-03 19:42:36 +02:00
guus b03a442444 styling nom 2025-08-03 19:40:03 +02:00
mark 28ee97782c fix context error 2025-08-03 14:04:10 +02:00
mark 7ad74db834 stop quota increase 2025-08-03 13:58:05 +02:00
mark 5ee177e4cf nominate animation 2025-08-03 13:56:41 +02:00
mark 80eb434cde dont double lower quota 2025-08-03 12:39:19 +02:00
mark 9f7981de65 removes client secret 2025-08-03 12:36:54 +02:00
mark aff31e4c1d fixes hyperlink bug 2025-08-03 12:36:27 +02:00
mark c7c808023a update quota 2025-08-03 12:01:48 +02:00
mark 898e275809 adds prod container and fixes build issues 2025-08-03 11:38:09 +02:00
mark 1aa28f978d fixes backend env 2025-08-03 11:08:38 +02:00
mark c21c98f6c4 nu met de juiste files toegevoegd 2025-08-03 10:50:49 +02:00
mark 54883d34c0 quota update. endpoint cleanup 2025-08-03 10:12:12 +02:00
mark 0cddc2dfb4 werkt het dan nu? 2025-08-02 19:04:35 +02:00
mark b1deaee4e6 fixes auth? 2025-08-02 15:35:18 +02:00
mark 78dc7be703 fixes?? nominations?? 2025-08-01 21:17:12 +02:00
mark 79adf3d3f5 remove auth status if not logged in 2025-08-01 18:32:48 +02:00
guus da288f20cd mv login page to root page 2025-07-31 23:30:14 +02:00
guus 0812a6b2cb styling fix nominaties 2025-07-30 22:50:30 +02:00
guus 507a65cfde changed styling nominaties 2025-07-30 22:48:02 +02:00
guus 90b523095a rm package.json in root 2025-07-28 20:24:10 +02:00
guus e1f50955f1 Merge pull request 'Data Access Layer' (#13) from add-data-layer into master
Reviewed-on: #13
Reviewed-by: guus <guus@noreply.%(DOMAIN)s>
2025-07-28 20:11:07 +02:00
mark c3a7a2d204 adds data access, login flow, navigation menu.
And some more stuff. :)
2025-07-27 10:26:40 +02:00
mark 31c6fd3265 adds track loading example 2025-07-19 19:53:24 +02:00
mark 7c92b33f91 adds type generation 2025-07-19 18:37:41 +02:00
mark ef0f82d516 first version of data fetcher 2025-07-19 10:50:41 +02:00
mark 041333d1da Merge pull request 'data link naar backend' (#11) from mark into master
Reviewed-on: #11
2025-07-15 17:36:35 +02:00
mark 6f867415f8 adds new config settings to example env 2025-07-15 17:36:19 +02:00
mark 43c6d7168b openapi defs for album and artist 2025-07-15 17:36:19 +02:00
mark 7a282e23f5 openapi definitions for track 2025-07-15 17:36:19 +02:00
mark 902243b969 adds openapi definitions to backend 2025-07-15 17:36:19 +02:00
guus 6f578eca6c voting page mock-up 2025-07-14 17:48:36 +02:00
guus 6b75e787c4 add routing links 2025-07-08 21:10:00 +02:00
guus 3c02ec930b update user grid cards 2025-07-08 17:26:01 +02:00
guus 499a35bf50 update lobby page design 2025-07-07 22:17:51 +02:00
guus a7ac9951a6 fix text outline for headers 2025-07-04 12:54:02 +02:00
guus 25d9f8ab7e add login page 2025-07-04 00:05:21 +02:00
mark 9263d88cee modules op de juiste plek pls make it stop aaaaaaaaaaaaaaaa 2025-07-02 20:49:12 +02:00
guus 389b6db432 add prettier tailwind 2025-07-02 19:14:47 +02:00
guus 142d2986b6 added fonts 2025-07-02 11:28:41 +02:00
guus 4134e3a234 eerste opzet nominatie pagina 2025-07-01 17:34:37 +02:00
guus 3185ca3c43 basic structure for pages 2025-07-01 14:48:56 +02:00
mark ca378790f1 first ugly version of authenticated backend requests.
needs some refactoring, but it works?
2025-06-30 13:22:27 +02:00
mark d9050b35ba api authentication yea 2025-06-29 01:38:21 +02:00
mark bfa1c7879b Authentication?
Voor een gegeven definitie van...
2025-06-25 16:38:22 +02:00
mark e7ca11d1bc playpause 2025-06-22 23:56:30 +02:00
mark dba9b58a6a rotating bg
with some custom classes.
2025-06-22 20:08:55 +02:00
mark 2a64d05b71 adds conical bg and tailwind.
not yet animated, but should be doable with @property.
2025-06-22 18:34:29 +02:00
mark ce5ec2a763 empty tag for wrapping component 2025-06-20 14:51:49 +02:00
mark 3576d85ac9 updated authentication classes 2025-06-20 14:37:34 +02:00
mark 970eb9e19e updated gitignore 2025-06-20 14:37:10 +02:00
mark dfc242084e first backend frontend split
with docker compose file
nextjs something something
also includes migrations
2025-06-20 14:06:00 +02:00
mark e7cc031588 Supports profile updates
only background for now, but hey.
2025-05-22 22:53:39 +02:00
mark a54f8947bb Default authentication on endpoints
Ik was er namelijk alweer eentje vergeten dus heh.
2025-05-22 22:30:06 +02:00
mark ce06b16881 track nomination
moet nog wel een hoop oude code weg, maar dat is een probleem voor later
2025-05-22 19:41:31 +02:00
mark 18dd107cfd fixes small bug when skipping 2025-05-22 15:20:11 +02:00
mark 3de8234fb9 adds vote posting
also moves some validation code to the model
2025-05-22 15:17:35 +02:00
mark 501044eef2 adds vote api 2025-05-20 23:18:57 +02:00
mark ddb99f5056 nu met een hoeveelheid meer profile 2025-05-20 23:09:34 +02:00
mark fed5612f46 adds albums 2025-05-20 22:53:31 +02:00
mark e41b9980a7 nu met 50 procent meer eindpunt authenticatie 2025-05-20 22:40:32 +02:00
mark 3223785a76 nu met 100 procent meer user endpoints 2025-05-20 22:35:18 +02:00
mark 8b2bbfd4d3 initial API views 2025-05-20 19:08:07 +02:00
mark 5a432ae1ef adds DRF 2025-05-20 19:07:25 +02:00
193 changed files with 5605 additions and 205 deletions
+6 -1
View File
@@ -3,6 +3,11 @@
db.sqlite3
*.pyc
*/__pycache__/
*/migrations/
albums/
wallpapers/
frontend/node_modules/
frontend/.next/
frontend/src/data/types.ts
backend/session_*.log
backend/*.csv
*.bak
View File
+3 -1
View File
@@ -1,11 +1,13 @@
SPOTIFY_ENCODED_ID=<Spotify-API-Token>
OIDC_RP_CLIENT_ID=<OIDC-Client-ID>
OIDC_RP_CLIENT_SECRET=<OIDC-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=<ISO-formatted date>
DATE_NOM_END=<ISO-formatted date>
DATE_VOTE_END=<ISO-formatted date>
DATE_VOTE_END=<ISO-formatted date>
+23
View File
@@ -0,0 +1,23 @@
-- sqlite3 -csv db.sqlite3 '.read export.sql' > export.csv
SELECT
CONCAT('https://open.spotify.com/track/', T.id) as spotify_id,
T.name as track_name,
A.name as artist_name,
(CAST(T.duration_ms AS REAL) / 86400000) as duration,
V.point_total,
V.vote_count,
CAST(V.point_total AS REAL) / CAST(V.vote_count AS REAL) as average_points,
U.username as nominated_by
FROM
(
SELECT track_id, SUM(points) as point_total, COUNT(*) as vote_count
FROM playlist_vote
GROUP BY track_id
) AS V
INNER JOIN playlist_track T ON V.track_id = T.id
INNER JOIN (
SELECT track_id, MIN(artist_id) as artist_id FROM playlist_track_artists GROUP BY track_id
) PTA ON T.id = PTA.track_id
INNER JOIN playlist_artist A ON PTA.artist_id = A.id
INNER JOIN auth_user U ON T.nominated_by_id = U.id
ORDER BY average_points DESC, vote_count DESC;
View File
+26
View File
@@ -0,0 +1,26 @@
"""
ASGI config for muzak project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from muzak.auth import QueryAuthMiddleware
import voting.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'muzak.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": QueryAuthMiddleware(
URLRouter(
voting.routing.websocket_urlpatterns
)
),
})
+58
View File
@@ -0,0 +1,58 @@
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from django.contrib.auth.models import User, AnonymousUser
from channels.db import database_sync_to_async
def get_user_from_token(token, log=False):
if log:
print("verifying token:", token)
backend = OIDCAuthenticationBackend()
try:
claims = backend.verify_token(token)
except Exception as e:
raise AuthenticationFailed(f'Invalid token: {e}')
return backend.filter_users_by_claims(claims).first()
class OIDCBearerTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if not auth.startswith('Bearer '):
return None
token = auth.split(' ')[1]
user = get_user_from_token(token)
if not user:
raise AuthenticationFailed(f'Unknown user: {token}')
return (user, None)
# @database_sync_to_async
# def get_user(user_id):
# try:
# return User.objects.get(id=user_id)
# except User.DoesNotExist:
# return AnonymousUser()
class QueryAuthMiddleware:
"""
Custom middleware that takes user from passed token.
"""
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
scope['user'] = AnonymousUser()
# print("Authing", scope)
# data = await receive()
# print("data: ", data)
# token = data.get('token')
# if token:
# user = database_sync_to_async(get_user_from_token(token, print=True))
# print("USER?", user)
# if user:
# scope['user'] = user
# else:
# pass
return await self.app(scope, receive, send)
@@ -13,11 +13,14 @@ SECRET_KEY = 'oefjei1lwe918Alkfwuf3ionwu-@kt)6m1e)ah$&^_i9y!qffhm-a$#m6+++'
DEBUG = True
ALLOWED_HOSTS = ['*']
CORS_ALLOW_ALL_ORIGINS = True
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
INSTALLED_APPS = [
'daphne',
'playlist.apps.PlaylistConfig',
'voting.apps.VotingConfig',
'django.contrib.admin',
'django.contrib.auth',
'mozilla_django_oidc',
@@ -26,11 +29,16 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django_htmx',
'rest_framework',
'corsheaders',
'drf_spectacular',
'channels',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -43,7 +51,7 @@ ROOT_URLCONF = 'muzak.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -57,6 +65,16 @@ TEMPLATES = [
]
WSGI_APPLICATION = 'muzak.wsgi.application'
ASGI_APPLICATION = 'muzak.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("valkey", 6379)],
},
},
}
DATABASES = {
'default': {
@@ -85,11 +103,31 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'muzak.auth.OIDCBearerTokenAuthentication',
'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'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')
OIDC_RP_CLIENT_SECRET = os.getenv('OIDC_RP_CLIENT_SECRET')
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv('OIDC_OP_AUTHORIZATION_ENDPOINT')
OIDC_OP_TOKEN_ENDPOINT = os.getenv('OIDC_OP_TOKEN_ENDPOINT')
OIDC_OP_USER_ENDPOINT = os.getenv('OIDC_OP_USER_ENDPOINT')
OIDC_RP_SIGN_ALGO = os.getenv('OIDC_RP_SIGN_ALGO')
OIDC_OP_JWKS_ENDPOINT = os.getenv('OIDC_OP_JWKS_ENDPOINT')
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
LOGIN_URL = "/"
@@ -103,4 +141,4 @@ USE_TZ = True
STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SESSION_COOKIE_AGE = 60*60*24*4
SESSION_COOKIE_AGE = 60*60*24*4
+1
View File
@@ -20,5 +20,6 @@ from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('oidc/', include('mozilla_django_oidc.urls')),
path('voting/', include('voting.urls')),
path('', include('playlist.urls')),
]
@@ -1,8 +1,10 @@
from django.contrib import admin
from django.utils.http import urlencode
from django.shortcuts import redirect
from .models import Track, Artist, Album, Background, Vote, Profile
from .models import Track, Artist, Album, Background, Vote, Profile, Session
class SessionAdmin(admin.ModelAdmin):
pass
class TrackAdmin(admin.ModelAdmin):
pass
class ArtistAdmin(admin.ModelAdmin):
@@ -34,3 +36,4 @@ admin.site.register(Album, AlbumAdmin)
admin.site.register(Background, BackgroundAdmin)
admin.site.register(Vote, VoteAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Session, SessionAdmin)
@@ -0,0 +1,48 @@
# Generated by Django 5.0.1 on 2024-02-04 22:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Artist',
fields=[
('id', models.CharField(max_length=128, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('popularity', models.SmallIntegerField(blank=True, null=True)),
('genres', models.JSONField()),
],
),
migrations.CreateModel(
name='Album',
fields=[
('id', models.CharField(max_length=128, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('album_type', models.CharField(max_length=255)),
('total_tracks', models.PositiveSmallIntegerField(null=True)),
('image', models.ImageField(upload_to='albums/')),
('artists', models.ManyToManyField(to='playlist.artist')),
],
),
migrations.CreateModel(
name='Track',
fields=[
('id', models.CharField(max_length=128, primary_key=True, serialize=False)),
('title', models.CharField(max_length=255)),
('explicit', models.BooleanField(default=False)),
('duration_ms', models.PositiveIntegerField(null=True)),
('popularity', models.PositiveIntegerField(null=True)),
('banter', models.TextField()),
('album', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.album')),
('artists', models.ManyToManyField(to='playlist.artist')),
],
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-05 21:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='track',
name='old',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-05 21:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('playlist', '0002_track_old'),
]
operations = [
migrations.RenameField(
model_name='track',
old_name='title',
new_name='name',
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.0.1 on 2024-02-05 22:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0003_rename_title_track_name'),
]
operations = [
migrations.AlterField(
model_name='artist',
name='genres',
field=models.JSONField(null=True),
),
migrations.AlterField(
model_name='track',
name='banter',
field=models.TextField(blank=True),
),
]
+28
View File
@@ -0,0 +1,28 @@
# Generated by Django 5.0.1 on 2024-02-07 12:26
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0004_alter_artist_genres_alter_track_banter'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Vote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('points', models.IntegerField()),
('track', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='playlist.track')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'track')},
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-07 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0005_vote'),
]
operations = [
migrations.AddField(
model_name='track',
name='banter_done',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-12 13:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0006_track_banter_done'),
]
operations = [
migrations.AddField(
model_name='album',
name='image_url',
field=models.URLField(null=True),
),
]
@@ -0,0 +1,32 @@
# Generated by Django 5.0.1 on 2024-02-21 13:26
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0007_album_image_url'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Background',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='wallpapers/')),
('repeat', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('background', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.background')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.0.1 on 2024-02-21 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0008_background_profile'),
]
operations = [
migrations.AddField(
model_name='background',
name='name',
field=models.CharField(default='Wallpaper', max_length=128),
preserve_default=False,
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-21 14:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0009_background_name'),
]
operations = [
migrations.AddField(
model_name='background',
name='color',
field=models.CharField(blank=True, max_length=32),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-21 14:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0010_background_color'),
]
operations = [
migrations.AlterField(
model_name='background',
name='image',
field=models.ImageField(null=True, upload_to='wallpapers/'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-02-21 14:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0011_alter_background_image'),
]
operations = [
migrations.AlterField(
model_name='background',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='wallpapers/'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.0.1 on 2024-02-26 18:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0012_alter_background_image'),
]
operations = [
migrations.AddField(
model_name='profile',
name='last_voted',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.track'),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.0.1 on 2024-02-26 20:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0013_profile_last_voted'),
]
operations = [
migrations.AddField(
model_name='profile',
name='quota',
field=models.FloatField(default=10.0),
),
migrations.AddField(
model_name='profile',
name='quota_last_updated',
field=models.DateTimeField(auto_now=True),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.0.1 on 2024-02-26 22:17
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0014_profile_quota_profile_quota_last_updated'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='quota_last_updated',
field=models.DateTimeField(default=django.utils.timezone.now),
),
]
@@ -0,0 +1,31 @@
# Generated by Django 5.0.1 on 2024-03-08 23:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0015_alter_profile_quota_last_updated'),
]
operations = [
migrations.AddField(
model_name='profile',
name='spt_access_token',
field=models.CharField(default='', max_length=128),
),
migrations.AddField(
model_name='profile',
name='spt_refresh_token',
field=models.CharField(default='', max_length=128),
),
migrations.CreateModel(
name='Playlist',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('tracks', models.ManyToManyField(to='playlist.track')),
],
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-03-08 23:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0016_profile_spt_access_token_profile_spt_refresh_token_and_more'),
]
operations = [
migrations.AddField(
model_name='profile',
name='spt_expires',
field=models.DateTimeField(default=None, null=True),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.0.1 on 2024-03-13 08:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0017_profile_spt_expires'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='spt_access_token',
field=models.CharField(default='', max_length=128, null=True),
),
migrations.AlterField(
model_name='profile',
name='spt_refresh_token',
field=models.CharField(default='', max_length=128, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-07-20 06:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0018_alter_profile_spt_access_token_and_more'),
]
operations = [
migrations.AddField(
model_name='vote',
name='skipped',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,31 @@
# Generated by Django 5.0.4 on 2024-07-21 07:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0019_vote_skipped'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='track',
name='nominated_by',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='profile',
name='spt_access_token',
field=models.CharField(blank=True, default='', max_length=256, null=True),
),
migrations.AlterField(
model_name='profile',
name='spt_refresh_token',
field=models.CharField(blank=True, default='', max_length=256, null=True),
),
]
@@ -0,0 +1,46 @@
# Generated by Django 5.0.4 on 2024-07-22 20:32
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0020_track_nominated_by_alter_profile_spt_access_token_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='track',
name='from_playlist',
field=models.CharField(blank=True, default=None, max_length=255, null=True),
),
migrations.AlterField(
model_name='track',
name='album',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.album'),
),
migrations.AlterField(
model_name='track',
name='banter',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='track',
name='duration_ms',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='track',
name='nominated_by',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='track',
name='popularity',
field=models.PositiveIntegerField(blank=True, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-07-23 11:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('playlist', '0021_track_from_playlist_alter_track_album_and_more'),
]
operations = [
migrations.AddField(
model_name='background',
name='cover',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,98 @@
# Generated by Django 5.2.4 on 2025-08-07 17:36
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0022_background_cover"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="vote",
name="points",
field=models.IntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(5),
]
),
),
migrations.CreateModel(
name="Session",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("opens", models.DateTimeField(default=None, null=True)),
("session_id", models.UUIDField(default=uuid.uuid4, editable=False)),
("seed", models.IntegerField(default=892933)),
(
"status",
models.CharField(
choices=[
("lobby", "Lobby"),
("voting", "Voting"),
("waiting_for_next_track", "Waiting for Next Track"),
("paused", "Paused"),
("voting_done", "Voting Done"),
],
default="lobby",
max_length=30,
),
),
("voting_open", models.BooleanField(default=False)),
(
"current_track",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="playlist.track",
),
),
(
"host",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"players_connected",
models.ManyToManyField(
related_name="sessions_joined", to=settings.AUTH_USER_MODEL
),
),
(
"players_wanted",
models.ManyToManyField(
related_name="players_wanted", to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.AddField(
model_name="vote",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="playlist.session",
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-09 06:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0023_alter_vote_points_session_vote_session"),
]
operations = [
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=689663),
),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-08-09 08:42
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0024_alter_session_seed"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="session",
name="players_connected",
field=models.ManyToManyField(
null=True, related_name="players_joined", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=804599),
),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-08-09 08:42
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0025_alter_session_players_connected_alter_session_seed"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="session",
name="players_connected",
field=models.ManyToManyField(
blank=True, related_name="players_joined", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=456463),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.2.4 on 2025-08-15 19:14
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0026_alter_session_players_connected_alter_session_seed"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=406213),
),
migrations.AlterUniqueTogether(
name="vote",
unique_together={("user", "track", "session")},
),
]
@@ -4,3 +4,4 @@ from .album import Album
from .vote import Vote
from .profile import Profile, Background
from .playlist import Playlist
from .session import Session
@@ -49,20 +49,20 @@ class Profile(models.Model):
self.quota = min(self.quota, MAX_QUOTA)
print(f"{self.user.username}: Er zijn {elapsed.total_seconds()}s voorbij, je nieuwe quota is {self.quota}")
self.quota_last_updated = timezone.now()
@property
def can_nominate(self):
"""Is this user allowed to nominate at this point in time?"""
end_date = os.getenv('DATE_NOM_END')
if self.user.is_superuser:
return (True,)
return (True, None)
if end_date:
end_date = datetime.fromisoformat(end_date)
if end_date <= datetime.now(tz=UTC):
return (False, "over")
if self.quota < 1:
return (False, "quota")
return (True,)
return (True, None)
@property
def can_vote(self):
@@ -86,7 +86,7 @@ class Profile(models.Model):
def init_spt(self, code):
"""Initial setting of spotify tokens from user accept code"""
data = spt.get_token_json(
grant_type='authorization_code',
grant_type='authorization_code',
code=code
)
self.save_spt(data)
@@ -109,7 +109,7 @@ class Profile(models.Model):
self.spt_refresh_token = refresh
self.spt_expires = timezone.now() + timedelta(seconds=expires - 300)
self.save()
def unlink_spt(self):
self.spt_access_token = None
self.spt_refresh_token = None
@@ -122,7 +122,7 @@ class Profile(models.Model):
self.spt_expires and
(timezone.now() < self.spt_expires)
)
def get_or_update_spt(self):
if self.spt_refresh_token:
if not self.can_spt:
@@ -131,7 +131,6 @@ class Profile(models.Model):
return None
def save(self, *args, **kwargs):
self.update_quota()
super().save(*args, **kwargs)
@receiver(post_save, sender=User)
+33
View File
@@ -0,0 +1,33 @@
from django.db import models
from django.contrib.auth.models import User
import uuid
import random
from datetime import datetime
class Session(models.Model):
def __str__(self):
return f"Session {self.session_id} - Host: {self.host.username}"
@property
def is_open(self):
return (datetime.now() < self.opens) and self.voting_open
opens = models.DateTimeField(null=True, default=None)
session_id = models.UUIDField(default=uuid.uuid4, editable=False)
host = models.ForeignKey(User, on_delete=models.CASCADE)
players_wanted = models.ManyToManyField(User, related_name='players_wanted')
players_connected= models.ManyToManyField(User, related_name='players_joined', blank=True)
seed = models.IntegerField(default=random.randint(1, 1000000))
status = models.CharField(
max_length=30,
choices=[
('lobby', 'Lobby'),
('voting', 'Voting'),
('waiting_for_next_track', 'Waiting for Next Track'),
('paused', 'Paused'),
('voting_done', 'Voting Done'),
],
default='lobby'
)
current_track = models.ForeignKey('Track', on_delete=models.SET_NULL, null=True, blank=True)
voting_open = models.BooleanField(default=False)
+149
View File
@@ -0,0 +1,149 @@
from django.db import models
from django.contrib.auth.models import User
from random import choice
from .album import Album
from .artist import Artist
from .vote import Vote
from playlist.spotify import spt
#from playlist.utils import track_from_json
import re
class TrackManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(old=False)
def from_json(self, cls, json):
try:
return cls.objects.get(pk=json["id"])
except cls.DoesNotExist:
keys = [f.name for f in cls._meta.get_fields()]
subset = {key:json[key] for key in set(keys) & set(json.keys())}
return cls.objects.create(**subset)
def track_from_json(self, json):
"""Creates the track, album and artist from spotify response."""
# Album artists
album_artists = []
for artist in json["album"]["artists"]:
album_artists.append(self.from_json(Artist, artist))
del json["album"]["artists"]
# Album
album = self.from_json(Album, json["album"])
album.image_url = json["album"]["images"][0]["url"]
album.save()
del json["album"]
for artist in album_artists:
album.artists.add(artist)
# Track artists
track_artists = []
for artist in json["artists"]:
track_artists.append(self.from_json(Artist, artist))
del json["artists"]
# Track
track = self.from_json(Track, json)
for artist in track_artists:
track.artists.add(artist)
track.album = album
track.save()
return track
def create_from_spotify(self, spotify_link, profile):
if re.match(r"^https:\/\/spotify.link.*", spotify_link):
spotify_link = spt.unshort(spotify_link)
m = re.match(r"^https:\/\/open.spotify.com\/track\/([a-zA-Z0-9]+)\??", spotify_link)
if m is None:
return None, Exception("Invalid Spotify track URL")
spotify_id = m.group(1)
try:
existing_track = self.model.all_tracks.get(pk=spotify_id)
if existing_track.old:
existing_track.old = False
existing_track.user = profile.user
existing_track.save()
return (existing_track, "Updated old track")
else:
return (None, "Track already exists")
except:
pass
try:
json = spt.get_song_info(spotify_id)
except Exception as e:
print(e)
return (None, "Error fetching track")
#return TemplateResponse(request, "Nominate.html", {"message_type": "error", "message": "Fout bij het ophalen van het liedje."})
track = self.track_from_json(json)
track.nominated_by = profile.user
track.save()
dup = track.is_duplicate_of()
if dup:
track.delete()
return (None, "Track already exists")
#get_banter.delay(track.id)
profile.quota -= 1
profile.save()
return (track, None)
class Track(models.Model):
def __str__(self):
s = f"'{self.name}'"
if self.artists.count() > 0:
artists = list(map(lambda a:a.name, self.artists.all()))
s += " door "
s += " & ".join(artists)
return s
@property
def artist(self):
if self.artists.count() > 0:
artists = list(map(lambda a:a.name, self.artists.all()))
return " & ".join(artists)
@property
def album_cover(self):
if self.album:
return self.album.image_url
@property
def name_sanitized(self):
return re.match("[^\-\(]*\w", self.name)[0]
@property
def link(self):
return f'https://open.spotify.com/track/{self.id}'
@property
def score(self):
return Vote.objects.filter(track=self.id).aggregate(rating=models.Avg("points"))["rating"]
def is_duplicate_of(self):
tracks = Track.objects.exclude(id=self.id).filter(name__startswith=self.name_sanitized)
artists = [artist.name for artist in self.artists.all()]
for artist in artists:
tracks = tracks.filter(artists__name__exact=artist)
return tracks.first()
id = models.CharField(primary_key=True, max_length=128)
nominated_by = models.ForeignKey(User, blank=True, null=True, default=None, on_delete=models.SET_NULL)
name = models.CharField(max_length=255)
artists = models.ManyToManyField(Artist)
album = models.ForeignKey(Album, blank=True, null=True, on_delete=models.SET_NULL)
explicit = models.BooleanField(default=False)
duration_ms = models.PositiveIntegerField(blank=True, null=True)
popularity = models.PositiveIntegerField(blank=True, null=True)
banter = models.TextField(null=True, blank=True)
banter_done = models.BooleanField(default=False)
old = models.BooleanField(default=False)
from_playlist = models.CharField(max_length=255, null=True, blank=True, default=None)
objects = TrackManager()
all_tracks = models.Manager()
+13
View File
@@ -0,0 +1,13 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from .session import Session
class Vote(models.Model):
class Meta:
unique_together = ["user", "track", "session"]
track = models.ForeignKey('Track', on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
skipped = models.BooleanField(default=False)
session = models.ForeignKey(Session, on_delete=models.CASCADE, null=True, default=None)
+7
View File
@@ -0,0 +1,7 @@
from .track import *
from .artist import *
from .user import *
from .album import *
from .profile import *
from .vote import *
from .session import *
+47
View File
@@ -0,0 +1,47 @@
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
fields = '__all__'
extra_kwargs = {
'url': {'view_name': 'api-album', 'lookup_field': 'id'},
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
}
+31
View File
@@ -0,0 +1,31 @@
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
fields = ['id', 'name', 'url']
extra_kwargs = {
'url': {'view_name': 'api-artist', 'lookup_field': 'id'},
}
+20
View File
@@ -0,0 +1,20 @@
from playlist.models import Profile, Background
from rest_framework import serializers
class BackgroundSerializer(serializers.ModelSerializer):
class Meta:
model = Background
fields = '__all__'
class ProfileSerializer(serializers.ModelSerializer):
background = BackgroundSerializer()
class Meta:
model = Profile
fields = '__all__'
class ProfileUpdateSerializer(serializers.ModelSerializer):
background = serializers.PrimaryKeyRelatedField(many=False, queryset=Background.objects.all())
class Meta:
model = Profile
fields = ['background']
+9
View File
@@ -0,0 +1,9 @@
from rest_framework import serializers
from playlist.models import Session
class SessionSerializer(serializers.ModelSerializer):
host = serializers.EmailField(source='host.email', read_only=True)
class Meta:
model = Session
fields = '__all__'
+61
View File
@@ -0,0 +1,61 @@
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):
artist = serializers.CharField()
album_cover = serializers.CharField()
class Meta:
model = Track
fields = '__all__'
extra_kwargs = {
'url': {'view_name': 'api-track', 'lookup_field': 'id'},
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
'album': {'view_name': 'api-album', 'lookup_field': 'id'},
'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'},
}
+7
View File
@@ -0,0 +1,7 @@
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
+28
View File
@@ -0,0 +1,28 @@
from rest_framework import serializers
from playlist.models import Vote, Track
class VoteSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Vote
fields = '__all__'
extra_kwargs = {
'url': {'view_name': 'api-vote', 'lookup_field': 'id'},
'track': {'view_name': 'api-track', 'lookup_field': 'id'},
'user': {'view_name': 'api-user', 'lookup_field': 'id'}
}
class VoteSaveSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ['track', 'points', 'skipped']
def create(self, validated_data):
"""
Create a new vote. Deletes a previous skipped vote if it exists.
"""
user = self.context['request'].user
track = validated_data['track']
previous_vote = Vote.objects.filter(user=user, track=track).first()
if previous_vote and previous_vote.skipped:
previous_vote.delete()
return Vote.objects.create(**validated_data)

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 556 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 462 B

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 451 B

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 749 B

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 411 B

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 394 B

After

Width:  |  Height:  |  Size: 394 B

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 524 B

Some files were not shown because too many files have changed in this diff Show More