Compare commits
98 Commits
2024
...
1d50bc3839
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d50bc3839 | |||
| 479dbe5c2d | |||
| 4e324502ae | |||
| d6cfdf66c6 | |||
| a64d9fea31 | |||
| b04a4732cf | |||
| bd71bce450 | |||
| cfb6b2609b | |||
| 564f2d47b4 | |||
| f22ff07577 | |||
| 47583e36dc | |||
| 7d54f6c5ce | |||
| 698c41544c | |||
| 5de174b162 | |||
| 341f7c8091 | |||
| 12344272ae | |||
| 762ec15a30 | |||
| c7eb174a01 | |||
| a1526ddc67 | |||
| cbc9be1f63 | |||
| 6216bd2c84 | |||
| 368628c070 | |||
| e0a03f00bf | |||
| 76b084b541 | |||
| 21d5f56a39 | |||
| 4c70a3f72d | |||
| b20e0c3980 | |||
| 8448a865a7 | |||
| b45bcf03ab | |||
| 421a004667 | |||
| 370507d5a6 | |||
| 67d8dac8c0 | |||
| 3eadd5c15d | |||
| 5ea702e7bb | |||
| 407abec1bf | |||
| b03a442444 | |||
| 28ee97782c | |||
| 7ad74db834 | |||
| 5ee177e4cf | |||
| 80eb434cde | |||
| 9f7981de65 | |||
| aff31e4c1d | |||
| c7c808023a | |||
| 898e275809 | |||
| 1aa28f978d | |||
| c21c98f6c4 | |||
| 54883d34c0 | |||
| 0cddc2dfb4 | |||
| b1deaee4e6 | |||
| 78dc7be703 | |||
| 79adf3d3f5 | |||
| da288f20cd | |||
| 0812a6b2cb | |||
| 507a65cfde | |||
| 90b523095a | |||
| e1f50955f1 | |||
| c3a7a2d204 | |||
| 31c6fd3265 | |||
| 7c92b33f91 | |||
| ef0f82d516 | |||
| 041333d1da | |||
| 6f867415f8 | |||
| 43c6d7168b | |||
| 7a282e23f5 | |||
| 902243b969 | |||
| 6f578eca6c | |||
| 6b75e787c4 | |||
| 3c02ec930b | |||
| 499a35bf50 | |||
| a7ac9951a6 | |||
| 25d9f8ab7e | |||
| 9263d88cee | |||
| 389b6db432 | |||
| 142d2986b6 | |||
| 4134e3a234 | |||
| 3185ca3c43 | |||
| ca378790f1 | |||
| d9050b35ba | |||
| bfa1c7879b | |||
| e7ca11d1bc | |||
| dba9b58a6a | |||
| 2a64d05b71 | |||
| ce5ec2a763 | |||
| 3576d85ac9 | |||
| 970eb9e19e | |||
| dfc242084e | |||
| e7cc031588 | |||
| a54f8947bb | |||
| ce06b16881 | |||
| 18dd107cfd | |||
| 3de8234fb9 | |||
| 501044eef2 | |||
| ddb99f5056 | |||
| fed5612f46 | |||
| e41b9980a7 | |||
| 3223785a76 | |||
| 8b2bbfd4d3 | |||
| 5a432ae1ef |
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
@@ -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,11 +29,16 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django_htmx',
|
'django_htmx',
|
||||||
|
'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',
|
||||||
@@ -43,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': [
|
||||||
@@ -57,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': {
|
||||||
@@ -85,11 +103,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 = "/"
|
||||||
@@ -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')),
|
||||||
]
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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):
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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 *
|
||||||
@@ -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'},
|
||||||
|
}
|
||||||
@@ -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'},
|
||||||
|
}
|
||||||
@@ -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']
|
||||||
@@ -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__'
|
||||||
@@ -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'},
|
||||||
|
}
|
||||||
@@ -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']
|
||||||
@@ -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 |