58 Commits

Author SHA1 Message Date
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
166 changed files with 4338 additions and 176 deletions
+3 -1
View File
@@ -3,6 +3,8 @@
db.sqlite3 db.sqlite3
*.pyc *.pyc
*/__pycache__/ */__pycache__/
*/migrations/
albums/ albums/
wallpapers/ wallpapers/
frontend/node_modules/
frontend/.next/
frontend/src/data/types.ts
View File
+2
View File
@@ -1,9 +1,11 @@
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>
View File
+23
View File
@@ -0,0 +1,23 @@
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
class OIDCBearerTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if not auth.startswith('Bearer '):
return None
token = auth.split(' ')[1]
backend = OIDCAuthenticationBackend()
try:
claims = backend.verify_token(token)
except Exception as e:
raise AuthenticationFailed(f'Invalid token: {e}')
user = backend.filter_users_by_claims(claims).first()
if not user:
raise AuthenticationFailed(f'Unknown user: {claims}')
return (user, None)
@@ -13,6 +13,7 @@ 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.
@@ -26,11 +27,15 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_htmx', 'django_htmx',
'rest_framework',
'corsheaders',
'drf_spectacular',
] ]
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',
@@ -85,11 +90,31 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', '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_ID = os.getenv('OIDC_RP_CLIENT_ID')
OIDC_RP_CLIENT_SECRET = os.getenv('OIDC_RP_CLIENT_SECRET') 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 = "/"
@@ -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),
),
]
@@ -55,14 +55,14 @@ class Profile(models.Model):
"""Is this user allowed to nominate at this point in time?""" """Is this user allowed to nominate at this point in time?"""
end_date = os.getenv('DATE_NOM_END') end_date = os.getenv('DATE_NOM_END')
if self.user.is_superuser: if self.user.is_superuser:
return (True,) return (True, None)
if end_date: if end_date:
end_date = datetime.fromisoformat(end_date) end_date = datetime.fromisoformat(end_date)
if end_date <= datetime.now(tz=UTC): if end_date <= datetime.now(tz=UTC):
return (False, "over") return (False, "over")
if self.quota < 1: if self.quota < 1:
return (False, "quota") return (False, "quota")
return (True,) return (True, None)
@property @property
def can_vote(self): def can_vote(self):
+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()
@@ -1,10 +1,11 @@
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
class Vote(models.Model): class Vote(models.Model):
class Meta: class Meta:
unique_together = ["user", "track"] unique_together = ["user", "track"]
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() points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
skipped = models.BooleanField(default=False) skipped = models.BooleanField(default=False)
+6
View File
@@ -0,0 +1,6 @@
from .track import *
from .artist import *
from .user import *
from .album import *
from .profile import *
from .vote 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']
+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

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 239 B

After

Width:  |  Height:  |  Size: 239 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 393 B

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 458 B

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