81 Commits

Author SHA1 Message Date
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
197 changed files with 6212 additions and 242 deletions
+4 -1
View File
@@ -3,6 +3,9 @@
db.sqlite3 db.sqlite3
*.pyc *.pyc
*/__pycache__/ */__pycache__/
*/migrations/
albums/ albums/
wallpapers/ wallpapers/
frontend/node_modules/
frontend/.next/
frontend/src/data/types.ts
backend/session_*.log
View File
+3 -1
View File
@@ -1,11 +1,13 @@
SPOTIFY_ENCODED_ID=<Spotify-API-Token> SPOTIFY_ENCODED_ID=<Spotify-API-Token>
OIDC_RP_CLIENT_ID=<OIDC-Client-ID> OIDC_RP_CLIENT_ID=<OIDC-Client-ID>
OIDC_RP_CLIENT_SECRET=<OIDC-Client-Secret> 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_AUTHORIZATION_ENDPOINT=https://auth.yourwebsite.com/application/o/authorize/
OIDC_OP_TOKEN_ENDPOINT=https://auth.yourwebsite.com/application/o/token/ OIDC_OP_TOKEN_ENDPOINT=https://auth.yourwebsite.com/application/o/token/
OIDC_OP_USER_ENDPOINT=https://auth.yourwebsite.com/application/o/userinfo/ 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_ENDPOINT=http://localhost:11434/api/generate
AI_MODEL=myModel AI_MODEL=myModel
DATE_OPEN=<ISO-formatted date> DATE_OPEN=<ISO-formatted date>
DATE_NOM_END=<ISO-formatted date> DATE_NOM_END=<ISO-formatted date>
DATE_VOTE_END=<ISO-formatted date> DATE_VOTE_END=<ISO-formatted date>
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 DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
CORS_ALLOW_ALL_ORIGINS = True
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work. CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
INSTALLED_APPS = [ INSTALLED_APPS = [
'daphne',
'playlist.apps.PlaylistConfig', 'playlist.apps.PlaylistConfig',
'voting.apps.VotingConfig',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'mozilla_django_oidc', 'mozilla_django_oidc',
@@ -26,12 +29,16 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_htmx', 'django_htmx',
'rest_framework' 'rest_framework',
'corsheaders',
'drf_spectacular',
'channels',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -44,7 +51,7 @@ ROOT_URLCONF = 'muzak.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -58,6 +65,16 @@ TEMPLATES = [
] ]
WSGI_APPLICATION = 'muzak.wsgi.application' WSGI_APPLICATION = 'muzak.wsgi.application'
ASGI_APPLICATION = 'muzak.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("valkey", 6379)],
},
},
}
DATABASES = { DATABASES = {
'default': { 'default': {
@@ -88,12 +105,20 @@ AUTHENTICATION_BACKENDS = (
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'muzak.auth.OIDCBearerTokenAuthentication',
'mozilla_django_oidc.contrib.drf.OIDCAuthentication', 'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
], ],
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', '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_ID = os.getenv('OIDC_RP_CLIENT_ID')
@@ -101,6 +126,8 @@ OIDC_RP_CLIENT_SECRET = os.getenv('OIDC_RP_CLIENT_SECRET')
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv('OIDC_OP_AUTHORIZATION_ENDPOINT') OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv('OIDC_OP_AUTHORIZATION_ENDPOINT')
OIDC_OP_TOKEN_ENDPOINT = os.getenv('OIDC_OP_TOKEN_ENDPOINT') OIDC_OP_TOKEN_ENDPOINT = os.getenv('OIDC_OP_TOKEN_ENDPOINT')
OIDC_OP_USER_ENDPOINT = os.getenv('OIDC_OP_USER_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 = "/" LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/"
LOGIN_URL = "/" LOGIN_URL = "/"
+1
View File
@@ -20,5 +20,6 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('oidc/', include('mozilla_django_oidc.urls')), path('oidc/', include('mozilla_django_oidc.urls')),
path('voting/', include('voting.urls')),
path('', include('playlist.urls')), path('', include('playlist.urls')),
] ]
View File
@@ -1,8 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.utils.http import urlencode from django.utils.http import urlencode
from django.shortcuts import redirect 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): class TrackAdmin(admin.ModelAdmin):
pass pass
class ArtistAdmin(admin.ModelAdmin): class ArtistAdmin(admin.ModelAdmin):
@@ -34,3 +36,4 @@ admin.site.register(Album, AlbumAdmin)
admin.site.register(Background, BackgroundAdmin) admin.site.register(Background, BackgroundAdmin)
admin.site.register(Vote, VoteAdmin) admin.site.register(Vote, VoteAdmin)
admin.site.register(Profile, ProfileAdmin) 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 .vote import Vote
from .profile import Profile, Background from .profile import Profile, Background
from .playlist import Playlist from .playlist import Playlist
from .session import Session
@@ -131,7 +131,6 @@ class Profile(models.Model):
return None return None
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.update_quota()
super().save(*args, **kwargs) super().save(*args, **kwargs)
@receiver(post_save, sender=User) @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)
@@ -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]
@@ -1,11 +1,13 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from .session import Session
class Vote(models.Model): class Vote(models.Model):
class Meta: class Meta:
unique_together = ["user", "track"] unique_together = ["user", "track", "session"]
track = models.ForeignKey('Track', on_delete=models.CASCADE) track = models.ForeignKey('Track', on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)]) points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
skipped = models.BooleanField(default=False) skipped = models.BooleanField(default=False)
session = models.ForeignKey(Session, on_delete=models.CASCADE, null=True, default=None)
@@ -4,3 +4,4 @@ from .user import *
from .album import * from .album import *
from .profile import * from .profile import *
from .vote 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'},
}
+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']

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