mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2025-12-11 10:02:22 +01:00
Compare commits
1 Commits
feature/lo
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
| d55ff6c8c6 |
@ -12,7 +12,7 @@ black:
|
||||
- python3 -m pip install --upgrade pip
|
||||
- curl -sSL https://install.python-poetry.org | python3 -
|
||||
- export PATH="/root/.local/bin:$PATH"
|
||||
- poetry install --with dev --no-root
|
||||
- poetry install --with dev
|
||||
script:
|
||||
- poetry run black --quiet --check marietje
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ if __name__ == "__main__":
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django # noqa
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
|
||||
@ -7,13 +7,13 @@ from .models import User
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(None, {"fields": ("username", "password", "queue")}),
|
||||
(_("Personal info"), {"fields": ("name", "email")}),
|
||||
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
|
||||
)
|
||||
list_display = ("username", "email", "name", "date_joined", "last_login", "is_staff")
|
||||
list_display = ("username", "email", "name", "date_joined", "last_login", "queue", "is_staff")
|
||||
search_fields = ("username", "name", "email")
|
||||
|
||||
def delete_model(self, request, user):
|
||||
|
||||
@ -28,6 +28,7 @@ class Command(BaseCommand):
|
||||
user.name = import_user["n"].strip()
|
||||
user.email = user.username + "@science.ru.nl"
|
||||
user.password = "md5$$" + import_user["p"]
|
||||
user.queue = get_first_queue()
|
||||
user.save()
|
||||
|
||||
if options["tsv_file"]:
|
||||
@ -44,6 +45,7 @@ class Command(BaseCommand):
|
||||
user.name = import_user[2].decode("utf-8", errors="ignore").strip()
|
||||
user.email = user.username + "@science.ru.nl"
|
||||
user.password = import_user[3].decode("utf-8", errors="strict")
|
||||
user.queue = get_first_queue()
|
||||
user.study = import_user[5].decode("utf-8", errors="ignore").strip()
|
||||
user.save()
|
||||
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-24 20:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_new_queue_mappings(apps, schema_editor):
|
||||
"""Before removing the old reference to Queue from User, we should move this to the newly created model."""
|
||||
User = apps.get_model("marietje", "User")
|
||||
UserQueue = apps.get_model("queues", "UserQueue")
|
||||
for user in User.objects.all():
|
||||
if user.queue is not None:
|
||||
UserQueue.objects.create(user=user, queue=user.queue)
|
||||
else:
|
||||
UserQueue.objects.create(user=user)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("marietje", "0008_alter_user_id"),
|
||||
("queues", "0012_userqueue_queuelogentry"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_new_queue_mappings,
|
||||
migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@ -1,16 +0,0 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-24 20:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("marietje", "0009_auto_20231124_2117"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="queue",
|
||||
),
|
||||
]
|
||||
@ -7,6 +7,9 @@ from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from marietje.utils import get_first_queue
|
||||
from queues.models import Queue
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
use_in_migrations = True
|
||||
@ -16,8 +19,9 @@ class UserManager(BaseUserManager):
|
||||
raise ValueError("The given username must be set")
|
||||
email = self.normalize_email(email)
|
||||
username = self.model.normalize_username(username)
|
||||
queue = get_first_queue()
|
||||
|
||||
user = self.model(username=username, email=email, **extra_fields)
|
||||
user = self.model(username=username, email=email, queue=queue, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
@ -76,6 +80,8 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
queue = models.ForeignKey(Queue, on_delete=models.SET_NULL, blank=True, null=True)
|
||||
|
||||
activation_token = models.TextField(_("activation token"), blank=True, null=True)
|
||||
|
||||
reset_token = models.TextField(_("reset token"), blank=True, null=True)
|
||||
|
||||
@ -4,7 +4,6 @@ from pathlib import Path
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'marietje',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@ -16,6 +15,7 @@ INSTALLED_APPS = [
|
||||
'rest_framework',
|
||||
'tinymce',
|
||||
'announcements',
|
||||
'marietje',
|
||||
'queues',
|
||||
'songs',
|
||||
'stats',
|
||||
@ -129,6 +129,7 @@ OAUTH2_PROVIDER = {
|
||||
"write": "Authenticated write access to the website",
|
||||
},
|
||||
}
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application"
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
STATIC_URL = '/static/'
|
||||
@ -163,3 +164,5 @@ TRUSTED_IP_RANGES = [
|
||||
]
|
||||
|
||||
DEFAULT_QUEUE = 1
|
||||
|
||||
OAUTH_2_APPLICATIONS_WITH_GAIN_AND_PEAK_PERMISSION = [2]
|
||||
|
||||
@ -38,6 +38,5 @@ urlpatterns = [
|
||||
path("beeldscherm/", partial(render, template_name="marietje/beeldscherm.html"), name="beeldscherm"),
|
||||
path("songs/", include(("songs.urls", "songs"), namespace="songs")),
|
||||
path("api/", include("marietje.api.urls")),
|
||||
path("playerapi/", include(("playerapi.urls", "playerapi"), namespace="playerapi")),
|
||||
path("stats/", include(("stats.urls", "stats"), namespace="stats")),
|
||||
]
|
||||
|
||||
82
marietje/marietje/utils.py
Normal file
82
marietje/marietje/utils.py
Normal file
@ -0,0 +1,82 @@
|
||||
import binascii
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
from queues.models import Queue, Playlist
|
||||
|
||||
|
||||
def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options):
|
||||
data = {
|
||||
"id": song.id,
|
||||
"artist": song.artist,
|
||||
"title": song.title,
|
||||
"duration": song.duration,
|
||||
}
|
||||
|
||||
if include_hash:
|
||||
data["hash"] = song.hash
|
||||
|
||||
if include_user is not None and song.user is not None and song.user.name:
|
||||
data["uploader_name"] = song.user.name
|
||||
|
||||
if include_replaygain:
|
||||
data["rg_gain"] = song.rg_gain
|
||||
data["rg_peak"] = song.rg_peak
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def playlist_song_to_dict(playlist_song, **options):
|
||||
user = options.get("user")
|
||||
return {
|
||||
"id": playlist_song.id,
|
||||
"requested_by": "Marietje" if playlist_song.user is None else playlist_song.user.name,
|
||||
"song": song_to_dict(playlist_song.song, **options),
|
||||
"can_move_down": playlist_song.user is not None and playlist_song.user == user,
|
||||
}
|
||||
|
||||
|
||||
# Send a file to bertha file storage.
|
||||
def send_to_bertha(file):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(settings.BERTHA_HOST)
|
||||
sock.sendall(struct.pack("<BQ", 4, file.size))
|
||||
|
||||
for chunk in file.chunks():
|
||||
sock.sendall(chunk)
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
song_hash = binascii.hexlify(sock.recv(64))
|
||||
sock.close()
|
||||
return song_hash
|
||||
|
||||
|
||||
def get_first_queue():
|
||||
queue = Queue.objects.first()
|
||||
if queue is None:
|
||||
playlist = Playlist()
|
||||
playlist.save()
|
||||
random_playlist = Playlist()
|
||||
random_playlist.save()
|
||||
queue = Queue(name="Queue", playlist=playlist, random_playlist=random_playlist)
|
||||
queue.save()
|
||||
return queue
|
||||
|
||||
|
||||
def bertha_stream(song_hash):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(settings.BERTHA_HOST)
|
||||
sock.sendall(struct.pack("<B", 2) + binascii.unhexlify(song_hash))
|
||||
data = sock.recv(4096)
|
||||
while data:
|
||||
yield data
|
||||
data = sock.recv(4096)
|
||||
sock.close()
|
||||
|
||||
|
||||
def get_from_bertha(song_hash):
|
||||
response = StreamingHttpResponse(bertha_stream(song_hash))
|
||||
response["Content-Disposition"] = 'attachment; filename="{}"'.format(song_hash)
|
||||
return response
|
||||
@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PlayerapiConfig(AppConfig):
|
||||
name = "playerapi"
|
||||
@ -1,15 +0,0 @@
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponseForbidden
|
||||
|
||||
from queues.models import Queue
|
||||
|
||||
|
||||
def token_required(function):
|
||||
def _dec(request):
|
||||
queue = get_object_or_404(Queue, id=request.POST.get("queue"))
|
||||
if not check_password(request.POST.get("player_token"), queue.player_token):
|
||||
return HttpResponseForbidden()
|
||||
return function(request)
|
||||
|
||||
return _dec
|
||||
@ -1,29 +0,0 @@
|
||||
def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options):
|
||||
data = {
|
||||
"id": song.id,
|
||||
"artist": song.artist,
|
||||
"title": song.title,
|
||||
"duration": song.duration,
|
||||
}
|
||||
|
||||
if include_hash:
|
||||
data["hash"] = song.hash
|
||||
|
||||
if include_user is not None and song.user is not None and song.user.name:
|
||||
data["uploader_name"] = song.user.name
|
||||
|
||||
if include_replaygain:
|
||||
data["rg_gain"] = song.rg_gain
|
||||
data["rg_peak"] = song.rg_peak
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def playlist_song_to_dict(playlist_song, **options):
|
||||
user = options.get("user")
|
||||
return {
|
||||
"id": playlist_song.id,
|
||||
"requested_by": "Marietje" if playlist_song.user is None else playlist_song.user.name,
|
||||
"song": song_to_dict(playlist_song.song, **options),
|
||||
"can_move_down": playlist_song.user is not None and playlist_song.user == user,
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "playerapi"
|
||||
|
||||
urlpatterns = [
|
||||
path("queue/", views.queue),
|
||||
path("play/", views.play),
|
||||
path("next/", views.next),
|
||||
path("analysed/", views.analysed),
|
||||
]
|
||||
@ -1,70 +0,0 @@
|
||||
from django.utils import timezone
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from queues.models import Queue
|
||||
from songs.models import Song
|
||||
|
||||
from .decorators import token_required
|
||||
from .services import playlist_song_to_dict
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@token_required
|
||||
def queue(request):
|
||||
current_queue = get_object_or_404(Queue, id=request.POST.get("queue"))
|
||||
commands = current_queue.queuecommand_set.all()
|
||||
response = JsonResponse(
|
||||
{
|
||||
"current_song": playlist_song_to_dict(
|
||||
current_queue.current_song(), include_hash=True, include_replaygain=True
|
||||
),
|
||||
"queue": [
|
||||
playlist_song_to_dict(playlist_song, include_hash=True, include_replaygain=True)
|
||||
for playlist_song in current_queue.queue()[:1]
|
||||
],
|
||||
"commands": [command.command for command in commands],
|
||||
}
|
||||
)
|
||||
for command in commands:
|
||||
command.delete()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@token_required
|
||||
def play(request):
|
||||
current_queue = get_object_or_404(Queue, id=request.POST.get("queue"))
|
||||
current_queue.started_at = timezone.now()
|
||||
current_queue.save()
|
||||
current_song = current_queue.current_song()
|
||||
current_song.played_at = current_queue.started_at
|
||||
current_song.save()
|
||||
|
||||
return JsonResponse({})
|
||||
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
@csrf_exempt
|
||||
@token_required
|
||||
def next(request):
|
||||
current_queue = get_object_or_404(Queue, id=request.POST.get("queue"))
|
||||
player_song = current_queue.current_song()
|
||||
player_song.state = 2
|
||||
player_song.save()
|
||||
return JsonResponse({})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@token_required
|
||||
def analysed(request):
|
||||
song = get_object_or_404(Song, id=request.POST.get("song"))
|
||||
if "gain" in request.POST:
|
||||
song.rg_gain = request.POST.get("gain")
|
||||
if "peak" in request.POST:
|
||||
song.rg_peak = request.POST.get("peak")
|
||||
song.save()
|
||||
return JsonResponse({})
|
||||
@ -1,18 +1,9 @@
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Queue, Playlist, PlaylistSong, QueueCommand
|
||||
|
||||
from .models import Queue, Playlist, PlaylistSong, QueueCommand, QueueLogEntry, UserQueue
|
||||
from marietje.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .services import get_queue_for_user
|
||||
|
||||
admin.site.register(Playlist)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@admin.register(Queue)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
@ -30,61 +21,3 @@ class PlaylistSongAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
admin.site.register(QueueCommand)
|
||||
|
||||
|
||||
@admin.register(QueueLogEntry)
|
||||
class QueueLogEntryAdmin(admin.ModelAdmin):
|
||||
"""Admin for log entries."""
|
||||
|
||||
list_display = [
|
||||
"timestamp",
|
||||
"queue",
|
||||
"action",
|
||||
"user",
|
||||
"description",
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
"queue",
|
||||
"action",
|
||||
("timestamp", admin.DateFieldListFilter),
|
||||
]
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Disable delete permission."""
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable add permission."""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Disable change permission."""
|
||||
return False
|
||||
|
||||
|
||||
class UserQueueInline(admin.StackedInline):
|
||||
model = UserQueue
|
||||
fields = ("queue",)
|
||||
|
||||
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(_("Personal info"), {"fields": ("name", "email")}),
|
||||
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
|
||||
)
|
||||
list_display = ("username", "email", "name", "date_joined", "last_login", "queue__queue", "is_staff")
|
||||
inlines = (UserQueueInline,)
|
||||
|
||||
def queue__queue(self, obj: User) -> Optional[Queue]:
|
||||
"""Retrieve the Queue for a User."""
|
||||
return get_queue_for_user(obj)
|
||||
|
||||
queue__queue.short_description = "queue"
|
||||
|
||||
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
@ -3,7 +3,7 @@ import time
|
||||
from rest_framework import serializers
|
||||
|
||||
from marietje.api.v1.serializers import UserRelatedFieldSerializer
|
||||
from queues.models import Queue, Playlist, PlaylistSong
|
||||
from queues.models import Queue, Playlist, PlaylistSong, QueueCommand
|
||||
from songs.api.v1.serializers import SongSerializer
|
||||
|
||||
|
||||
@ -28,8 +28,8 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class QueueSerializer(serializers.ModelSerializer):
|
||||
current_song = serializers.SerializerMethodField()
|
||||
queue = serializers.SerializerMethodField()
|
||||
current_song = serializers.SerializerMethodField(read_only=True)
|
||||
queue = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_current_song(self, queue):
|
||||
return PlaylistSongSerializer(queue.current_song()).data
|
||||
@ -48,3 +48,18 @@ class QueueSerializer(serializers.ModelSerializer):
|
||||
"queue",
|
||||
"started_at",
|
||||
]
|
||||
read_only_fiels = [
|
||||
"id",
|
||||
"current_song",
|
||||
"queue",
|
||||
]
|
||||
|
||||
|
||||
class QueueCommandSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = QueueCommand
|
||||
fields = [
|
||||
"id",
|
||||
"queue",
|
||||
"command",
|
||||
]
|
||||
|
||||
@ -11,10 +11,14 @@ from queues.api.v1.views import (
|
||||
QueueVolumeDownAPIView,
|
||||
QueueVolumeUpAPIView,
|
||||
QueueMuteAPIView,
|
||||
QueueCommandListAPIView,
|
||||
QueueCommandDestroyAPIView,
|
||||
QueueUpdateAPIView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("current/", QueueAPIView.as_view(), name="queue_current"),
|
||||
path("<int:pk>/", QueueUpdateAPIView.as_view(), name="queue_update"),
|
||||
path("current/skip/", QueueSkipAPIView.as_view(), name="queue_skip"),
|
||||
path("current/request/", QueueRequestAPIView.as_view(), name="queue_request"),
|
||||
path("current/volume-down/", QueueVolumeDownAPIView.as_view(), name="queue_volume_down"),
|
||||
@ -24,4 +28,6 @@ urlpatterns = [
|
||||
path("playlists/<int:pk>/", PlaylistRetrieveAPIView.as_view(), name="playlist_retrieve"),
|
||||
path("playlist-song/<int:id>/move-down/", PlaylistSongMoveDownAPIView.as_view(), name="playlist_song_move_down"),
|
||||
path("playlist-song/<int:id>/cancel/", PlaylistSongCancelAPIView.as_view(), name="playlist_song_cancel"),
|
||||
path("<int:pk>/commands/", QueueCommandListAPIView.as_view(), name="queue_command_list"),
|
||||
path("commands/<int:pk>/", QueueCommandDestroyAPIView.as_view(), name="queue_command_destroy"),
|
||||
]
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
from django.db.models import Q
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404, CreateAPIView, DestroyAPIView
|
||||
from oauth2_provider.views.mixins import ClientProtectedResourceMixin
|
||||
from rest_framework import status, mixins
|
||||
from rest_framework.generics import (
|
||||
ListAPIView,
|
||||
RetrieveAPIView,
|
||||
get_object_or_404,
|
||||
CreateAPIView,
|
||||
DestroyAPIView,
|
||||
UpdateAPIView,
|
||||
)
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
@ -7,7 +15,12 @@ from marietje.api.openapi import CustomAutoSchema
|
||||
from marietje.api.permissions import IsAuthenticatedOrTokenHasScopeForMethod
|
||||
from django.http import Http404
|
||||
|
||||
from queues.api.v1.serializers import PlaylistSerializer, QueueSerializer, PlaylistSongSerializer
|
||||
from queues.api.v1.serializers import (
|
||||
PlaylistSerializer,
|
||||
QueueSerializer,
|
||||
PlaylistSongSerializer,
|
||||
QueueCommandSerializer,
|
||||
)
|
||||
from queues.exceptions import RequestException
|
||||
from queues.models import Playlist, PlaylistSong, QueueCommand, Queue
|
||||
from queues.services import get_user_or_default_queue
|
||||
@ -48,13 +61,58 @@ class QueueAPIView(APIView):
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
def get_object(self):
|
||||
queue = get_user_or_default_queue(self.request)
|
||||
if queue is None:
|
||||
raise Http404()
|
||||
return queue
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
queue = self.get_object()
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
class QueueUpdateAPIView(ClientProtectedResourceMixin, UpdateAPIView):
|
||||
serializer_class = QueueSerializer
|
||||
queryset = Queue.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"PUT": ["write"], "PATCH": ["write"]}
|
||||
schema = CustomAutoSchema(
|
||||
response_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "int", "example": 1},
|
||||
"name": {"type": "string", "example": "string"},
|
||||
"playlist": {"type": "int", "example": 1},
|
||||
"random_playlist": {"type": "int", "example": 1},
|
||||
"current_song": {"$ref": "#/components/schemas/PlaylistSong"},
|
||||
"queue": {"type": "array", "items": {"$ref": "#/components/schemas/PlaylistSong"}},
|
||||
"started_at": {"type": "string", "format": "date-time", "nullable": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def put(self, request, **kwargs):
|
||||
queue = self.get_object()
|
||||
if queue.oauth_client is None or queue.oauth_client != request.auth.application:
|
||||
return Response(
|
||||
{"detail": "Unauthorized"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
return super(QueueUpdateAPIView, self).put(request, **kwargs)
|
||||
|
||||
def patch(self, request, **kwargs):
|
||||
queue = self.get_object()
|
||||
if queue.oauth_client is None or queue.oauth_client != request.auth.application:
|
||||
return Response(
|
||||
{"detail": "Unauthorized"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
return super(QueueUpdateAPIView, self).patch(request, **kwargs)
|
||||
|
||||
|
||||
class QueueSkipAPIView(APIView):
|
||||
serializer_class = QueueSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
@ -82,16 +140,17 @@ class QueueSkipAPIView(APIView):
|
||||
return Response(status=404)
|
||||
|
||||
playlist_song = queue.current_song()
|
||||
if (
|
||||
request.user is not None
|
||||
and playlist_song.user != request.user
|
||||
and not request.user.has_perm("queues.can_skip")
|
||||
):
|
||||
if request.user is not None:
|
||||
if playlist_song.user != request.user and not request.user.has_perm("queues.can_skip"):
|
||||
return Response(status=403)
|
||||
elif request.auth is not None:
|
||||
if queue.oauth_client is None or request.auth.application != queue.oauth_client:
|
||||
return Response(status=403)
|
||||
else:
|
||||
return Response(status=403)
|
||||
|
||||
playlist_song.state = 2
|
||||
playlist_song.save()
|
||||
queue.log_action(request.user, "next", "Skipped to next song.")
|
||||
|
||||
return Response(status=200, data=QueueSerializer(queue).data)
|
||||
|
||||
@ -113,18 +172,7 @@ class PlaylistSongMoveDownAPIView(APIView):
|
||||
and not request.user.has_perm("queues.can_move")
|
||||
):
|
||||
return Response(status=403)
|
||||
|
||||
playlist_song.move_down()
|
||||
|
||||
for queue in Queue.objects.filter(
|
||||
Q(playlist=playlist_song.playlist) | Q(random_playlist=playlist_song.playlist)
|
||||
):
|
||||
queue.log_action(
|
||||
request.user,
|
||||
"down",
|
||||
'Moved song "{}" of playlist "{}" down.'.format(playlist_song.song, playlist_song.playlist),
|
||||
)
|
||||
|
||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||
|
||||
|
||||
@ -144,18 +192,7 @@ class PlaylistSongCancelAPIView(DestroyAPIView):
|
||||
and not request.user.has_perm("queues.can_cancel")
|
||||
):
|
||||
return Response(status=403)
|
||||
|
||||
playlist_song.delete()
|
||||
|
||||
for queue in Queue.objects.filter(
|
||||
Q(playlist=playlist_song.playlist) | Q(random_playlist=playlist_song.playlist)
|
||||
):
|
||||
queue.log_action(
|
||||
request.user,
|
||||
"cancel",
|
||||
'Cancelled song "{}" of playlist "{}".'.format(playlist_song.song, playlist_song.playlist),
|
||||
)
|
||||
|
||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||
|
||||
|
||||
@ -189,8 +226,6 @@ class QueueRequestAPIView(CreateAPIView):
|
||||
except RequestException as e:
|
||||
return Response(data={"success": False, "errorMessage": str(e)})
|
||||
|
||||
queue.log_action(request.user, "request_song", "Requested song {}.".format(song))
|
||||
|
||||
request_counter.labels(queue=queue.name).inc()
|
||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||
|
||||
@ -222,11 +257,7 @@ class QueueVolumeDownAPIView(APIView):
|
||||
return Response(status=404)
|
||||
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
||||
return Response(status=403)
|
||||
|
||||
QueueCommand.objects.create(queue=queue, command="volume_down")
|
||||
|
||||
queue.log_action(request.user, "volume_down", "Reduced the volume of {}.".format(queue))
|
||||
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
@ -257,11 +288,7 @@ class QueueVolumeUpAPIView(APIView):
|
||||
return Response(status=404)
|
||||
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
||||
return Response(status=403)
|
||||
|
||||
QueueCommand.objects.create(queue=queue, command="volume_up")
|
||||
|
||||
queue.log_action(request.user, "volume_up", "Increased the volume of {}.".format(queue))
|
||||
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
@ -292,9 +319,43 @@ class QueueMuteAPIView(APIView):
|
||||
return Response(status=404)
|
||||
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
||||
return Response(status=403)
|
||||
|
||||
QueueCommand.objects.create(queue=queue, command="mute")
|
||||
|
||||
queue.log_action(request.user, "mute", "Muted the volume of {}.".format(queue))
|
||||
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
class QueueCommandListAPIView(ClientProtectedResourceMixin, ListAPIView):
|
||||
serializer_class = QueueCommandSerializer
|
||||
queryset = QueueCommand.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"GET": ["read"]}
|
||||
|
||||
def get_queryset(self):
|
||||
queue = get_object_or_404(Queue, pk=self.kwargs.get("pk"))
|
||||
self.queryset.filter(queue=queue)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
queue = get_object_or_404(Queue, pk=kwargs.get("pk"))
|
||||
if queue.oauth_client is None or queue.oauth_client != request.auth.application:
|
||||
return Response(
|
||||
{"detail": "Unauthorized"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
return super(QueueCommandListAPIView, self).get(request, **kwargs)
|
||||
|
||||
|
||||
class QueueCommandDestroyAPIView(ClientProtectedResourceMixin, DestroyAPIView):
|
||||
serializer_class = QueueCommandSerializer
|
||||
queryset = QueueCommand.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"DELETE": ["write"]}
|
||||
|
||||
def delete(self, request, **kwargs):
|
||||
queue_command = self.get_object()
|
||||
if queue_command.queue.oauth_client is None or queue_command.queue.oauth_client != request.auth.application:
|
||||
return Response(
|
||||
{"detail": "Unauthorized"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
return super(QueueCommandDestroyAPIView, self).delete(request, **kwargs)
|
||||
|
||||
27
marietje/queues/migrations/0012_queue_oauth_client.py
Normal file
27
marietje/queues/migrations/0012_queue_oauth_client.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-05 14:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
("queues", "0011_alter_playlistsong_playlist"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="queue",
|
||||
name="oauth_client",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The OAuth2 client that may read and write commands for this Queue.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="queues",
|
||||
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,64 +0,0 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-24 20:17
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("queues", "0011_alter_playlistsong_playlist"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserQueue",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
(
|
||||
"queue",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="users",
|
||||
to="queues.queue",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="queue_new",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="QueueLogEntry",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("action", models.CharField(max_length=255)),
|
||||
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||
("description", models.CharField(max_length=255)),
|
||||
(
|
||||
"queue",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="logs", to="queues.queue"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "player log entry",
|
||||
"verbose_name_plural": "player log entries",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,22 +0,0 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-24 20:19
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("queues", "0012_userqueue_queuelogentry"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="userqueue",
|
||||
name="user",
|
||||
field=models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="queue", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,14 +1,12 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from oauth2_provider.models import Application
|
||||
|
||||
from queues.exceptions import RequestException
|
||||
from songs.models import Song
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Playlist(models.Model):
|
||||
def __str__(self):
|
||||
@ -84,6 +82,14 @@ class Queue(models.Model):
|
||||
)
|
||||
started_at = models.DateTimeField(blank=True, null=True)
|
||||
player_token = models.TextField(blank=True, null=True)
|
||||
oauth_client = models.ForeignKey(
|
||||
Application,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="The OAuth2 client that may read and write commands for this Queue.",
|
||||
related_name="queues",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def get_songs(self):
|
||||
self.fill_random_queue()
|
||||
@ -146,41 +152,10 @@ class Queue(models.Model):
|
||||
playlist_song.save()
|
||||
song_count += 1
|
||||
|
||||
def log_action(self, user: User, action: str, description: str) -> "QueueLogEntry":
|
||||
"""
|
||||
Log a queue action.
|
||||
|
||||
:param user: The user performing the action.
|
||||
:param action: An identifier of the action performed.
|
||||
:param description: An optional description for the action.
|
||||
:return: The created QueueLogEntry object.
|
||||
"""
|
||||
return QueueLogEntry.objects.create(
|
||||
queue=self,
|
||||
user=user,
|
||||
action=action,
|
||||
description=description,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class UserQueue(models.Model):
|
||||
"""
|
||||
UserQueue model.
|
||||
|
||||
This model connects a user to its queue.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="queue")
|
||||
queue = models.ForeignKey(Queue, on_delete=models.SET_NULL, null=True, blank=True, related_name="users")
|
||||
|
||||
def __str__(self):
|
||||
"""Convert this object to string."""
|
||||
return "Queue for user {}".format(self.user)
|
||||
|
||||
|
||||
class QueueCommand(models.Model):
|
||||
queue = models.ForeignKey(
|
||||
Queue,
|
||||
@ -191,20 +166,3 @@ class QueueCommand(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.command)
|
||||
|
||||
|
||||
class QueueLogEntry(models.Model):
|
||||
"""Model for logging queue events."""
|
||||
|
||||
queue = models.ForeignKey(Queue, on_delete=models.CASCADE, related_name="logs")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
||||
action = models.CharField(max_length=255)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
description = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.queue} {self.action} by {self.user} at {self.timestamp}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "player log entry"
|
||||
verbose_name_plural = "player log entries"
|
||||
|
||||
@ -1,44 +1,15 @@
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from queues.models import Queue, Playlist
|
||||
from queues.models import Queue
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_user_or_default_queue(request) -> Queue:
|
||||
"""Get the user or default queue from a request."""
|
||||
def get_user_or_default_queue(request):
|
||||
"""Get the user or default queue."""
|
||||
if request.user is None:
|
||||
return get_default_queue()
|
||||
else:
|
||||
return get_queue_for_user(request.user)
|
||||
return request.user.queue
|
||||
|
||||
|
||||
def get_queue_for_user(user: User) -> Optional[Queue]:
|
||||
"""Get the queue for a User."""
|
||||
if user.queue is None:
|
||||
return None
|
||||
else:
|
||||
return user.queue.queue
|
||||
|
||||
|
||||
def get_default_queue() -> Queue:
|
||||
def get_default_queue():
|
||||
"""Get the default queue."""
|
||||
try:
|
||||
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)
|
||||
except Queue.DoesNotExist:
|
||||
return get_first_queue()
|
||||
|
||||
|
||||
def get_first_queue() -> Queue:
|
||||
"""Get the first Queue object or create one."""
|
||||
queue = Queue.objects.first()
|
||||
if queue is not None:
|
||||
return queue
|
||||
|
||||
playlist = Playlist.objects.create()
|
||||
random_playlist = Playlist.objects.create()
|
||||
return Queue.objects.create(name="Queue", playlist=playlist, random_playlist=random_playlist)
|
||||
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from queues.models import UserQueue
|
||||
from queues.services import get_default_queue
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_default_queue(sender, instance, created, **kwargs):
|
||||
"""Create a UserQueue object when a User gets created."""
|
||||
if created:
|
||||
user_queue, user_queue_created = UserQueue.objects.get_or_create(user=instance)
|
||||
if user_queue_created:
|
||||
user_queue.queue = get_default_queue()
|
||||
user_queue.save()
|
||||
@ -5,11 +5,12 @@ from songs.models import Song, ReportNote
|
||||
|
||||
|
||||
class SongSerializer(serializers.ModelSerializer):
|
||||
user = UserRelatedFieldSerializer()
|
||||
user = UserRelatedFieldSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Song
|
||||
fields = ["id", "artist", "title", "duration", "hash", "user", "rg_gain", "rg_peak"]
|
||||
read_only_fields = ["id", "duration", "hash", "user"]
|
||||
|
||||
|
||||
class ReportNoteSerializer(serializers.ModelSerializer):
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import SongsListAPIView, SongRetrieveAPIView, SongUploadAPIView, ReportNoteCreateAPIView
|
||||
from .views import SongsListAPIView, SongRetrieveUpdateAPIView, SongUploadAPIView, ReportNoteCreateAPIView
|
||||
|
||||
urlpatterns = [
|
||||
path("", SongsListAPIView.as_view(), name="song_list"),
|
||||
path("<int:pk>/", SongRetrieveAPIView.as_view(), name="song_retrieve"),
|
||||
path("<int:pk>/", SongRetrieveUpdateAPIView.as_view(), name="song_retrieve_update"),
|
||||
path("report-notes/", ReportNoteCreateAPIView.as_view(), name="report_note_create"),
|
||||
path("upload/", SongUploadAPIView.as_view(), name="song_upload"),
|
||||
]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView, CreateAPIView
|
||||
from rest_framework import filters
|
||||
from rest_framework.generics import ListAPIView, RetrieveUpdateAPIView, CreateAPIView
|
||||
from rest_framework import filters, status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
@ -27,11 +28,35 @@ class SongsListAPIView(ListAPIView):
|
||||
]
|
||||
|
||||
|
||||
class SongRetrieveAPIView(RetrieveAPIView):
|
||||
class SongRetrieveUpdateAPIView(RetrieveUpdateAPIView):
|
||||
serializer_class = SongSerializer
|
||||
queryset = Song.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"GET": ["read"]}
|
||||
required_scopes_for_method = {"GET": ["read"], "PUT": ["write"], "PATCH": ["write"]}
|
||||
|
||||
def put(self, request, **kwargs):
|
||||
if (
|
||||
request.auth is None
|
||||
or request.auth.application.id not in settings.OAUTH_2_APPLICATIONS_WITH_GAIN_AND_PEAK_PERMISSION
|
||||
):
|
||||
return Response(
|
||||
{"detail": "Unauthorized"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
return super(SongRetrieveUpdateAPIView, self).put(request, **kwargs)
|
||||
|
||||
def patch(self, request, **kwargs):
|
||||
if (
|
||||
request.auth is None
|
||||
or request.auth.application.id not in settings.OAUTH_2_APPLICATIONS_WITH_GAIN_AND_PEAK_PERMISSION
|
||||
):
|
||||
return Response(
|
||||
{"detail": "Unauthorized"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
return super(SongRetrieveUpdateAPIView, self).patch(request, **kwargs)
|
||||
|
||||
|
||||
class ReportNoteCreateAPIView(CreateAPIView):
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import binascii
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from marietje.utils import send_to_bertha
|
||||
from queues.models import PlaylistSong
|
||||
from songs.models import Song
|
||||
from django.db.models.functions import Coalesce
|
||||
@ -16,20 +11,6 @@ class UploadException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def send_to_bertha(file):
|
||||
"""Send a file to Berthad file storage."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(settings.BERTHA_HOST)
|
||||
sock.sendall(struct.pack("<BQ", 4, file.size))
|
||||
|
||||
for chunk in file.chunks():
|
||||
sock.sendall(chunk)
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
song_hash = binascii.hexlify(sock.recv(64))
|
||||
sock.close()
|
||||
return song_hash
|
||||
|
||||
|
||||
def is_regular_queue(ps):
|
||||
if not ps.played_at:
|
||||
# Request is from the old times, assume good
|
||||
|
||||
@ -11,7 +11,7 @@ authors = [
|
||||
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
|
||||
]
|
||||
maintainers = [
|
||||
"Kees van Kempen <ru@keesvankempen.nl>",
|
||||
"Kees van Kempen <ru@keesvankempen.nl",
|
||||
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
Reference in New Issue
Block a user