mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2025-12-10 09:12:23 +01:00
Marietje 4.1: Addition of Django REST framework, Swagger, Dark mode and updates to Django and Bootstrap
This commit is contained in:
@ -7,16 +7,17 @@ admin.site.register(Playlist)
|
||||
|
||||
@admin.register(Queue)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'playlist', 'random_playlist')
|
||||
list_display = ("name", "playlist", "random_playlist")
|
||||
|
||||
|
||||
@admin.register(PlaylistSong)
|
||||
class PlaylistSongAdmin(admin.ModelAdmin):
|
||||
list_display = ('playlist', 'song', 'user', 'state', 'played_at')
|
||||
list_display_links = ('song',)
|
||||
list_filter = ('playlist', 'state', 'user')
|
||||
search_fields = ('song__title', 'song__artist', 'user__name')
|
||||
autocomplete_fields = ('user',)
|
||||
readonly_fields = ('song',)
|
||||
list_display = ("playlist", "song", "user", "state", "played_at")
|
||||
list_display_links = ("song",)
|
||||
list_filter = ("playlist", "state", "user")
|
||||
search_fields = ("song__title", "song__artist", "user__name")
|
||||
autocomplete_fields = ("user",)
|
||||
readonly_fields = ("song",)
|
||||
|
||||
|
||||
admin.site.register(QueueCommand)
|
||||
|
||||
0
marietje/queues/api/v1/__init__.py
Normal file
0
marietje/queues/api/v1/__init__.py
Normal file
50
marietje/queues/api/v1/serializers.py
Normal file
50
marietje/queues/api/v1/serializers.py
Normal file
@ -0,0 +1,50 @@
|
||||
import time
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from marietje.api.v1.serializers import UserRelatedFieldSerializer
|
||||
from queues.models import Queue, Playlist, PlaylistSong
|
||||
from songs.api.v1.serializers import SongSerializer
|
||||
|
||||
|
||||
class PlaylistSongSerializer(serializers.ModelSerializer):
|
||||
song = SongSerializer()
|
||||
user = UserRelatedFieldSerializer()
|
||||
|
||||
class Meta:
|
||||
model = PlaylistSong
|
||||
fields = ["id", "playlist", "song", "user", "played_at"]
|
||||
|
||||
|
||||
class PlaylistSerializer(serializers.ModelSerializer):
|
||||
songs = PlaylistSongSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Playlist
|
||||
fields = [
|
||||
"id",
|
||||
"songs",
|
||||
]
|
||||
|
||||
|
||||
class QueueSerializer(serializers.ModelSerializer):
|
||||
current_song = serializers.SerializerMethodField()
|
||||
queue = serializers.SerializerMethodField()
|
||||
|
||||
def get_current_song(self, queue):
|
||||
return PlaylistSongSerializer(queue.current_song()).data
|
||||
|
||||
def get_queue(self, queue):
|
||||
return PlaylistSongSerializer(queue.queue(), many=True).data
|
||||
|
||||
class Meta:
|
||||
model = Queue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"playlist",
|
||||
"random_playlist",
|
||||
"current_song",
|
||||
"queue",
|
||||
"started_at",
|
||||
]
|
||||
27
marietje/queues/api/v1/urls.py
Normal file
27
marietje/queues/api/v1/urls.py
Normal file
@ -0,0 +1,27 @@
|
||||
from django.urls import path
|
||||
|
||||
from queues.api.v1.views import (
|
||||
PlaylistListAPIView,
|
||||
PlaylistRetrieveAPIView,
|
||||
QueueAPIView,
|
||||
QueueSkipAPIView,
|
||||
PlaylistSongMoveDownAPIView,
|
||||
PlaylistSongCancelAPIView,
|
||||
QueueRequestAPIView,
|
||||
QueueVolumeDownAPIView,
|
||||
QueueVolumeUpAPIView,
|
||||
QueueMuteAPIView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("current/", QueueAPIView.as_view(), name="queue_current"),
|
||||
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"),
|
||||
path("current/volume-up/", QueueVolumeUpAPIView.as_view(), name="queue_volume_up"),
|
||||
path("current/mute/", QueueMuteAPIView.as_view(), name="queue_mute"),
|
||||
path("playlists/", PlaylistListAPIView.as_view(), name="playlist_list"),
|
||||
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"),
|
||||
]
|
||||
262
marietje/queues/api/v1/views.py
Normal file
262
marietje/queues/api/v1/views.py
Normal file
@ -0,0 +1,262 @@
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404, CreateAPIView, DestroyAPIView
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
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.exceptions import RequestException
|
||||
from queues.models import Playlist, PlaylistSong, QueueCommand
|
||||
from queues.services import get_user_or_default_queue
|
||||
from songs.counters import request_counter
|
||||
from songs.models import Song
|
||||
|
||||
|
||||
class PlaylistListAPIView(ListAPIView):
|
||||
serializer_class = PlaylistSerializer
|
||||
queryset = Playlist.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"GET": ["read"]}
|
||||
|
||||
|
||||
class PlaylistRetrieveAPIView(RetrieveAPIView):
|
||||
serializer_class = PlaylistSerializer
|
||||
queryset = Playlist.objects.all()
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"GET": ["read"]}
|
||||
|
||||
|
||||
class QueueAPIView(APIView):
|
||||
serializer_class = QueueSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {"GET": ["read"]}
|
||||
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 get(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
if queue is None:
|
||||
raise Http404()
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
class QueueSkipAPIView(APIView):
|
||||
serializer_class = QueueSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"POST": ["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 post(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
if queue is None:
|
||||
return Response(status=404)
|
||||
|
||||
playlist_song = request.user.queue.current_song()
|
||||
if (
|
||||
request.user is not None
|
||||
and playlist_song.user != request.user
|
||||
and not request.user.has_perm("queues.can_skip")
|
||||
):
|
||||
return Response(status=403)
|
||||
|
||||
playlist_song.state = 2
|
||||
playlist_song.save()
|
||||
|
||||
return Response(status=200, data=QueueSerializer(queue).data)
|
||||
|
||||
|
||||
class PlaylistSongMoveDownAPIView(APIView):
|
||||
serializer_class = PlaylistSongSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"PATCH": ["write"],
|
||||
}
|
||||
schema = CustomAutoSchema(response_schema={"$ref": "#/components/schemas/PlaylistSong"})
|
||||
|
||||
def patch(self, request, **kwargs):
|
||||
playlist_song_id = kwargs.get("id")
|
||||
playlist_song = get_object_or_404(PlaylistSong, id=playlist_song_id)
|
||||
if (
|
||||
request.user is not None
|
||||
and playlist_song.user != request.user
|
||||
and not request.user.has_perm("queues.can_move")
|
||||
):
|
||||
return Response(status=403)
|
||||
playlist_song.move_down()
|
||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||
|
||||
|
||||
class PlaylistSongCancelAPIView(DestroyAPIView):
|
||||
serializer_class = PlaylistSongSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"DELETE": ["write"],
|
||||
}
|
||||
|
||||
def delete(self, request, **kwargs):
|
||||
playlist_song_id = kwargs.get("id")
|
||||
playlist_song = get_object_or_404(PlaylistSong, id=playlist_song_id)
|
||||
if (
|
||||
request.user is not None
|
||||
and playlist_song.user != request.user
|
||||
and not request.user.has_perm("queues.can_cancel")
|
||||
):
|
||||
return Response(status=403)
|
||||
playlist_song.delete()
|
||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||
|
||||
|
||||
class QueueRequestAPIView(CreateAPIView):
|
||||
serializer_class = PlaylistSongSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"POST": ["write"],
|
||||
}
|
||||
schema = CustomAutoSchema(
|
||||
request_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"song": {"type": "int", "example": 1},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
if queue is None:
|
||||
return Response(status=404)
|
||||
song_id = request.data.get("song", None)
|
||||
if song_id is None:
|
||||
return Response(status=400, data={"success": False, "errorMessage": "Song ID not set."})
|
||||
|
||||
song = get_object_or_404(Song, id=song_id, deleted=False)
|
||||
|
||||
try:
|
||||
playlist_song = queue.request(song, request.user)
|
||||
except RequestException as e:
|
||||
return Response(data={"success": False, "errorMessage": str(e)})
|
||||
|
||||
request_counter.labels(queue=queue.name).inc()
|
||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||
|
||||
|
||||
class QueueVolumeDownAPIView(APIView):
|
||||
serializer_class = QueueSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"POST": ["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 post(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
if queue is None:
|
||||
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")
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
class QueueVolumeUpAPIView(APIView):
|
||||
serializer_class = QueueSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"POST": ["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 post(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
if queue is None:
|
||||
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")
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
|
||||
|
||||
class QueueMuteAPIView(APIView):
|
||||
serializer_class = QueueSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
|
||||
required_scopes_for_method = {
|
||||
"POST": ["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 post(self, request, **kwargs):
|
||||
queue = get_user_or_default_queue(request)
|
||||
if queue is None:
|
||||
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")
|
||||
return Response(status=200, data=self.serializer_class(queue).data)
|
||||
@ -2,4 +2,5 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class QueuesConfig(AppConfig):
|
||||
name = 'queues'
|
||||
name = "queues"
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
|
||||
2
marietje/queues/exceptions.py
Normal file
2
marietje/queues/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class RequestException(Exception):
|
||||
pass
|
||||
@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-04 09:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('queues', '0009_unlimited_queue_length_perm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='playlist',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='playlistsong',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queue',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='queuecommand',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-04 16:58
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('queues', '0010_alter_playlist_id_alter_playlistsong_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='playlistsong',
|
||||
name='playlist',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='songs', to='queues.playlist'),
|
||||
),
|
||||
]
|
||||
@ -3,11 +3,13 @@ from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from queues.exceptions import RequestException
|
||||
from songs.models import Song
|
||||
|
||||
|
||||
class Playlist(models.Model):
|
||||
def __str__(self):
|
||||
return 'Playlist #' + str(self.id)
|
||||
return "Playlist #" + str(self.id)
|
||||
|
||||
|
||||
class PlaylistSong(models.Model):
|
||||
@ -16,6 +18,7 @@ class PlaylistSong(models.Model):
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="songs",
|
||||
)
|
||||
song = models.ForeignKey(
|
||||
Song,
|
||||
@ -37,14 +40,13 @@ class PlaylistSong(models.Model):
|
||||
# 2: Played.
|
||||
# 3: Cancelled.
|
||||
STATECHOICE = (
|
||||
(0, 'Queued'),
|
||||
(1, 'Playing'),
|
||||
(2, 'Played'),
|
||||
(3, 'Cancelled'),
|
||||
(0, "Queued"),
|
||||
(1, "Playing"),
|
||||
(2, "Played"),
|
||||
(3, "Cancelled"),
|
||||
)
|
||||
state = models.IntegerField(default=0, db_index=True, choices=STATECHOICE)
|
||||
|
||||
|
||||
def move_down(self):
|
||||
other_song = PlaylistSong.objects.filter(playlist=self.playlist, id__gt=self.id).first()
|
||||
old_id = self.id
|
||||
@ -54,17 +56,17 @@ class PlaylistSong(models.Model):
|
||||
other_song.save()
|
||||
|
||||
def __str__(self):
|
||||
return 'Playlist #' + str(self.playlist_id) + ': ' + str(self.song)
|
||||
return "Playlist #" + str(self.playlist_id) + ": " + str(self.song)
|
||||
|
||||
|
||||
class Queue(models.Model):
|
||||
class Meta:
|
||||
permissions = (
|
||||
('can_skip', 'Can skip the currently playing song'),
|
||||
('can_move', 'Can move all songs in the queue'),
|
||||
('can_cancel', 'Can cancel all songs in the queue'),
|
||||
('can_control_volume', 'Can control the volume of Marietje'),
|
||||
('unlimited_queue_length', 'Is unlimited by maximum queue length'),
|
||||
("can_skip", "Can skip the currently playing song"),
|
||||
("can_move", "Can move all songs in the queue"),
|
||||
("can_cancel", "Can cancel all songs in the queue"),
|
||||
("can_control_volume", "Can control the volume of Marietje"),
|
||||
("unlimited_queue_length", "Is unlimited by maximum queue length"),
|
||||
)
|
||||
|
||||
name = models.TextField()
|
||||
@ -75,35 +77,26 @@ class Queue(models.Model):
|
||||
null=True,
|
||||
)
|
||||
random_playlist = models.ForeignKey(
|
||||
Playlist,
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='random_playlist_set'
|
||||
Playlist, on_delete=models.SET_NULL, blank=True, null=True, related_name="random_playlist_set"
|
||||
)
|
||||
started_at = models.DateTimeField(blank=True, null=True)
|
||||
songs = None
|
||||
player_token = models.TextField(blank=True, null=True)
|
||||
|
||||
def get_songs(self):
|
||||
if self.songs is None:
|
||||
self.fill_random_queue()
|
||||
self.songs = PlaylistSong.objects\
|
||||
.filter(Q(playlist=self.playlist_id) | Q(playlist_id=self.random_playlist_id),
|
||||
Q(state=0) | Q(state=1))\
|
||||
.order_by('-state', 'playlist_id', 'id')\
|
||||
.select_related('song', 'user')
|
||||
return self.songs
|
||||
self.fill_random_queue()
|
||||
return (
|
||||
PlaylistSong.objects.filter(
|
||||
Q(playlist=self.playlist_id) | Q(playlist_id=self.random_playlist_id), Q(state=0) | Q(state=1)
|
||||
)
|
||||
.order_by("-state", "playlist_id", "id")
|
||||
.select_related("song", "user")
|
||||
)
|
||||
|
||||
def current_song(self):
|
||||
songs = self.get_songs()
|
||||
if not songs:
|
||||
return None
|
||||
song = songs[0]
|
||||
if song.state != 1:
|
||||
song.state = 1
|
||||
song.save()
|
||||
return song
|
||||
return songs[0]
|
||||
|
||||
def queue(self):
|
||||
songs = self.get_songs()
|
||||
@ -112,37 +105,38 @@ class Queue(models.Model):
|
||||
return songs[1:]
|
||||
|
||||
def request(self, song, user):
|
||||
if not user.has_perm('queues.unlimited_queue_length'):
|
||||
playlist_songs = PlaylistSong.objects.filter(playlist=self.playlist, state=0).order_by('id')
|
||||
if user is not None and not user.has_perm("queues.unlimited_queue_length"):
|
||||
playlist_songs = PlaylistSong.objects.filter(playlist=self.playlist, state=0).order_by("id")
|
||||
|
||||
seconds_in_a_row = sum(ps.song.duration for ps in playlist_songs if ps.user == user)
|
||||
msg = 'You cannot request more than ' + str(settings.MAX_MINUTES_IN_A_ROW) + ' minutes in a row.'
|
||||
msg = "You cannot request more than " + str(settings.MAX_MINUTES_IN_A_ROW) + " minutes in a row."
|
||||
if settings.LIMIT_ALWAYS:
|
||||
if seconds_in_a_row > settings.MAX_MINUTES_IN_A_ROW * 60:
|
||||
return msg
|
||||
raise RequestException(msg)
|
||||
else:
|
||||
now = timezone.now()
|
||||
if seconds_in_a_row > 0 and \
|
||||
seconds_in_a_row + song.duration > settings.MAX_MINUTES_IN_A_ROW * 60 and \
|
||||
settings.LIMIT_HOURS[0] <= now.hour < settings.LIMIT_HOURS[1]:
|
||||
return msg
|
||||
if (
|
||||
seconds_in_a_row > 0
|
||||
and seconds_in_a_row + song.duration > settings.MAX_MINUTES_IN_A_ROW * 60
|
||||
and settings.LIMIT_HOURS[0] <= now.hour < settings.LIMIT_HOURS[1]
|
||||
):
|
||||
raise RequestException(msg)
|
||||
|
||||
if {ps for ps in playlist_songs if ps.song == song}:
|
||||
return 'This song is already in the queue.'
|
||||
raise RequestException("This song is already in the queue.")
|
||||
|
||||
playlist_song = PlaylistSong(playlist=self.playlist, song=song, user=user)
|
||||
playlist_song.save()
|
||||
playlist_song = PlaylistSong.objects.create(playlist=self.playlist, song=song, user=user)
|
||||
|
||||
# If the song was auto-queue'd, then remove it from the auto-queue
|
||||
autolist_songs = PlaylistSong.objects.filter(playlist=self.random_playlist, state=0, song=song)
|
||||
autolist_songs.delete()
|
||||
|
||||
return None
|
||||
return playlist_song
|
||||
|
||||
def fill_random_queue(self):
|
||||
song_count = PlaylistSong.objects.filter(playlist_id=self.random_playlist_id, state=0).count()
|
||||
while song_count < 5:
|
||||
song = Song.objects.filter(deleted=False).order_by('?').first()
|
||||
song = Song.objects.filter(deleted=False).order_by("?").first()
|
||||
if song is None:
|
||||
return
|
||||
playlist_song = PlaylistSong(playlist=self.random_playlist, song=song, user=None)
|
||||
|
||||
15
marietje/queues/services.py
Normal file
15
marietje/queues/services.py
Normal file
@ -0,0 +1,15 @@
|
||||
from queues.models import Queue
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_user_or_default_queue(request):
|
||||
"""Get the user or default queue."""
|
||||
if request.user is None:
|
||||
return get_default_queue()
|
||||
else:
|
||||
return request.user.queue
|
||||
|
||||
|
||||
def get_default_queue():
|
||||
"""Get the default queue."""
|
||||
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)
|
||||
@ -1,123 +1,634 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "marietje/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Queue{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="navbar navbar-expand navbar-default navbar-light border-bottom">
|
||||
<div class="container">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="queue-tab" data-bs-toggle="tab" data-bs-target="#queue"
|
||||
type="button" role="tab" aria-controls="queue" aria-selected="true">Queue
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item me-3" role="presentation">
|
||||
<button class="nav-link" id="request-tab" data-bs-toggle="tab" data-bs-target="#request"
|
||||
type="button" role="tab" aria-controls="request" aria-selected="false">Request
|
||||
</button>
|
||||
</li>
|
||||
<li id="infobar-buttons" class="nav-item d-flex justify-content-center align-items-center">
|
||||
{% if perms.queues.can_control_volume %}
|
||||
<button type="button" id="mute" class="btn nav-btn btn-sm block-button" onclick="mute();">
|
||||
<i class="fa-solid fa-volume-xmark"></i>
|
||||
</button>
|
||||
<button type="button" id="volume_down" class="btn navbar-btn btn-sm block-button"
|
||||
onclick="volume_down();">
|
||||
<i class="fa-solid fa-volume-low"></i>
|
||||
</button>
|
||||
<button type="button" id="volume_up" class="btn navbar-btn btn-sm block-button"
|
||||
onclick="volume_up();">
|
||||
<i class="fa-solid fa-volume-high"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.queues.can_skip %}
|
||||
<button type="button" id="skip" class="btn navbar-btn btn-sm block-button" onclick="skip();">
|
||||
<i class="fa-solid fa-forward-fast"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav navbar-right hidden-xs">
|
||||
<li class="nav-item me-3">
|
||||
<p class="navbar-text mb-0 start-queue hidden-sm hidden-xs"></p>
|
||||
</li>
|
||||
<li class="nav-item me-3">
|
||||
<p class="navbar-text mb-0 end-queue"></p>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<p class="navbar-text mb-0 duration-queue"></p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<br><br>
|
||||
<div class="alert-location">
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="queue" role="tabpanel" aria-labelledby="queue-tab">
|
||||
<div id="queue-container">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr class="table-header-style">
|
||||
<td class="col-md-4">Artist</td>
|
||||
<td class="col-md-4">Title</td>
|
||||
<td class="col-md-2 d-sm-table-cell d-none">Requested By</td>
|
||||
<td class="col-md-1 text-info d-sm-table-cell d-none" style="cursor: pointer;">
|
||||
<span v-if="playsIn" id="timeswitch" class="btn btn-link p-0" v-on:click="playsIn = false">Plays In</span>
|
||||
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays at</span>
|
||||
</td>
|
||||
<td class="col-md-1 control-icons">Control</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="queuebody">
|
||||
<template v-for="(song, index) in queue">
|
||||
<tr :class="{ marietjequeue: (song.user === null), currentsong: (index === 0), 'fw-bold': (index === 0) }">
|
||||
<td class="artist"><% song.song.artist %></td>
|
||||
<td class="title"><% song.song.title %></td>
|
||||
<td class="d-sm-table-cell d-none requested-by">
|
||||
<template v-if="song.user === null">
|
||||
Marietje
|
||||
</template>
|
||||
<template v-else>
|
||||
<% song.user.name %>
|
||||
</template>
|
||||
</td>
|
||||
<td class="d-sm-table-cell d-none plays-at" style="text-align: right">
|
||||
<template v-if="song.time_until_song_seconds !== null && song.time_until_song_seconds > 0 && playsIn === true">
|
||||
<% song.time_until_song_seconds.secondsToMMSS() %>
|
||||
</template>
|
||||
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
|
||||
<% song.plays_at.timestampToHHMMSS() %>
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)"><i
|
||||
class="fa-solid fa-arrow-up"></i></a>
|
||||
<a href="#" v-else class="invisible"><i class="fa-solid fa-arrow-up"></i></a>
|
||||
|
||||
<a href="#" v-if="song.can_move_down" v-on:click="move_down(song.id)"><i
|
||||
class="fa-solid fa-arrow-down"></i></a>
|
||||
<a href="#" v-else class="invisible"><i class="fa-solid fa-arrow-down"></i></a>
|
||||
|
||||
<nav class="navbar navbar-default navbar-fixed-top" style="top: 50px; border-top: 1px solid #bbbbbb; max-height: 50px; box-shadow: 2px 2px 4px #BBBBBB;">
|
||||
<div class="container">
|
||||
<ul class="navbar-nav" style="max-height: 50px; margin: 0;">
|
||||
<li class="btn-toolbar nav navbar-nav" style="margin: 0;">
|
||||
<button id="request-button" class="btn navbar-btn btn-primary">
|
||||
Request
|
||||
</button>
|
||||
<div id="infobar-buttons" class="btn-group">
|
||||
{% if perms.queues.can_control_volume %}
|
||||
<button type="button" id="mute" class="btn navbar-btn btn-sm block-button">
|
||||
<span id="mute-button-span" class="glyphicon glyphicon-volume-off"></span>
|
||||
</button>
|
||||
<button type="button" id="volume_down" class="btn navbar-btn btn-sm block-button">
|
||||
<span id="voldown-button-span" class="glyphicon glyphicon-volume-down"></span>
|
||||
</button>
|
||||
<button type="button" id="volume_up" class="btn navbar-btn btn-sm block-button">
|
||||
<span id="volup-button-span" class="glyphicon glyphicon-volume-up"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.queues.can_skip %}
|
||||
<button type="button" id="skip" class="btn navbar-btn btn-sm block-button">
|
||||
<span id="skip-button-span" class="glyphicon glyphicon-fast-forward"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right hidden-xs">
|
||||
<li>
|
||||
<div class="infobar">
|
||||
<p class="navbar-text start-queue hidden-sm hidden-xs"></p>
|
||||
<p class="navbar-text end-queue"></p>
|
||||
<div class="navbar-text">
|
||||
<p class="duration-queue"></p>
|
||||
<a href="#" v-if="song.can_delete" v-on:click="cancel_song(song.id)"><i
|
||||
class="fa-solid fa-trash-can"></i></a>
|
||||
<a href="#" v-else class="invisible"><i class="fa-solid fa-trash-can"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<br><br>
|
||||
<div class="alert-location">
|
||||
</div>
|
||||
<div id="request-container" class="hidden">
|
||||
<table id="request-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Title</th>
|
||||
<th>Uploader</th>
|
||||
<th style="text-align: right;">Length</th>
|
||||
<th>Report</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2"><input id="search-all" class="search-input" type="text"></th>
|
||||
<th colspan="3"><input id="search-uploader" class="search-input" type="text"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="3" class="ts-pager form-horizontal">
|
||||
<button type="button" class="btn first"><i class="icon-step-backward glyphicon glyphicon-step-backward"></i></button>
|
||||
<button type="button" class="btn prev"><i class="icon-arrow-left glyphicon glyphicon-backward"></i></button>
|
||||
<button type="button" class="btn next"><i class="icon-arrow-right glyphicon glyphicon-forward"></i></button>
|
||||
<button type="button" class="btn last"><i class="icon-step-forward glyphicon glyphicon-step-forward"></i></button>
|
||||
<select class="pagesize input-mini" title="Select page size">
|
||||
<option selected value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
</select>
|
||||
<select class="pagenum input-mini" title="Select page number"></select>
|
||||
</th>
|
||||
<th colspan="2"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="queue-container">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr class="table-header-style">
|
||||
<td class="col-md-4">Artist</td>
|
||||
<td class="col-md-4">Title</td>
|
||||
<td class="col-md-2 hidden-xs">Requested By</td>
|
||||
<td class="col-md-1 hidden-xs text-info" style="cursor: pointer;">
|
||||
<span id="timeswitch" class="btn-link" >Plays In</span>
|
||||
</td>
|
||||
<td class="col-md-1 control-icons">Control</td>
|
||||
</tr>
|
||||
<tr class="currentsong" style="font-weight: bold">
|
||||
<td class="artist"></td>
|
||||
<td class="title"></td>
|
||||
<td class="requested-by hidden-xs"></td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="queuebody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="request" role="tabpanel" aria-labelledby="request-tab">
|
||||
<div id="request-container">
|
||||
<table id="request-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Title</th>
|
||||
<th>Uploader</th>
|
||||
<th>Length</th>
|
||||
<th>Report</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="5"><input id="search-all" class="search-input" type="text"
|
||||
v-model="search_input"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="3" class="ts-pager form-horizontal">
|
||||
<button v-if="page_number === 1" type="button" class="btn first disabled"><i
|
||||
class="fa-solid fa-backward-fast"></i></button>
|
||||
<button v-else v-on:click="update_page(1);" type="button" class="btn first"><i
|
||||
class="fa-solid fa-backward-fast"></i></button>
|
||||
|
||||
<script type="text/javascript" src="{% static 'js/js.cookie-2.1.3.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/queue.js' %}?1"></script>
|
||||
<script type="text/javascript">
|
||||
var csrf_token = "{{ csrf_token }}";
|
||||
var canSkip = {{ perms.queues.can_skip|yesno:"1,0" }};
|
||||
var canCancel = {{ perms.queues.can_cancel|yesno:"1,0" }};
|
||||
var canMoveSongs = {{ perms.queues.can_move|yesno:"1,0" }};
|
||||
</script>
|
||||
<button v-if="page_number === 1" type="button" class="btn prev disabled"><i
|
||||
class="fa-solid fa-backward"></i></button>
|
||||
<button v-else v-on:click="update_page(page_number - 1);" type="button"
|
||||
class="btn prev"><i class="fa-solid fa-backward"></i></button>
|
||||
|
||||
<button v-if="page_number === number_of_pages" type="button" class="btn next disabled">
|
||||
<i class="fa-solid fa-forward"></i></button>
|
||||
<button v-else v-on:click="update_page(page_number + 1);" type="button"
|
||||
class="btn next"><i class="fa-solid fa-forward"></i></button>
|
||||
|
||||
<button v-if="page_number === number_of_pages" type="button" class="btn last disabled">
|
||||
<i class="fa-solid fa-forward-fast"></i></button>
|
||||
<button v-else v-on:click="update_page(number_of_pages);" type="button"
|
||||
class="btn last"><i class="fa-solid fa-forward-fast"></i></button>
|
||||
|
||||
<select class="pagesize input-mini" title="Select page size" v-model="page_size">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
</select>
|
||||
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
|
||||
<template v-for="(i, index) in number_of_pages">
|
||||
<option :value="i"><% i %></option>
|
||||
</template>
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
<template v-for="(song, index) in songs">
|
||||
<tr>
|
||||
<td>
|
||||
<% song.artist %>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" v-on:click="request_song(song.id);"><% song.title %></a>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="song.user === null">
|
||||
Marietje
|
||||
</template>
|
||||
<template v-else>
|
||||
<% song.user.name %>
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<% song.duration.secondsToMMSS() %>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" v-on:click="report_song(song.id);">
|
||||
⚑
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript">
|
||||
const CAN_SKIP = {{ perms.queues.can_skip|yesno:"1,0" }};
|
||||
const CAN_CANCEL = {{ perms.queues.can_cancel|yesno:"1,0" }};
|
||||
const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }};
|
||||
</script>
|
||||
<script>
|
||||
const queue_vue = new Vue({
|
||||
el: '#queue-container',
|
||||
delimiters: ['<%', '%>'],
|
||||
data: {
|
||||
current_song: null,
|
||||
queue: [],
|
||||
user_data: null,
|
||||
refreshing: true,
|
||||
refreshTimer: null,
|
||||
clockInterval: null,
|
||||
started_at: null,
|
||||
playsIn: true,
|
||||
},
|
||||
mounted() {
|
||||
this.clockInterval = setInterval(this.update_song_times, 1000);
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.clockInterval);
|
||||
},
|
||||
created() {
|
||||
fetch('/api/v1/users/me/').then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(data => {
|
||||
this.user_data = data;
|
||||
}).then(() => {
|
||||
this.refreshing = false;
|
||||
this.refresh();
|
||||
}).catch(() => {
|
||||
tata.error('', 'User details failed to fetch, please reload this page to try again.');
|
||||
}).finally(() => {
|
||||
this.refreshing = false;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
infoBar() {
|
||||
let infoBar = {
|
||||
start_personal_queue: 0,
|
||||
length_personal_queue: 0,
|
||||
length_total_queue: 0,
|
||||
end_personal_queue: 0,
|
||||
max_length: 45,
|
||||
}
|
||||
for (let i = 0; i < this.queue.length; i++) {
|
||||
const current_song = this.queue[i];
|
||||
infoBar['length_total_queue'] = infoBar['length_total_queue'] + current_song.song.duration;
|
||||
if (current_song.user !== null && current_song.user.id === this.user_data.id) {
|
||||
infoBar['length_personal_queue'] = infoBar['length_personal_queue'] + current_song.song.duration;
|
||||
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
|
||||
if (infoBar['start_personal_queue'] === 0) {
|
||||
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
return infoBar;
|
||||
},
|
||||
play_next_song_at() {
|
||||
if (this.started_at !== null && this.current_song !== null) {
|
||||
return this.started_at + this.current_song.song.duration;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update_song_times() {
|
||||
const now_in_seconds = Math.round((new Date()).getTime() / 1000);
|
||||
let total_song_length = 0;
|
||||
for (let i = 0; i < this.queue.length; i++) {
|
||||
if (this.started_at === null) {
|
||||
this.queue[i].time_until_song_seconds = null;
|
||||
this.queue[i].plays_at = null;
|
||||
this.queue[i].played = false;
|
||||
} else {
|
||||
this.queue[i].time_until_song_seconds = total_song_length;
|
||||
this.queue[i].plays_at = now_in_seconds + total_song_length;
|
||||
this.queue[i].played = this.queue[i].plays_at <= now_in_seconds;
|
||||
if (i === 0) {
|
||||
total_song_length += this.queue[i].song.duration - (now_in_seconds - this.started_at);
|
||||
} else {
|
||||
total_song_length += this.queue[i].song.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
if (!this.refreshing) {
|
||||
this.refreshing = true;
|
||||
clearTimeout(this.refreshTimer);
|
||||
fetch('/api/v1/queues/current/').then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(data => {
|
||||
this.current_song = data.current_song;
|
||||
this.started_at = Math.round((new Date(data.started_at).getTime()) / 1000);
|
||||
let newQueue = data.queue;
|
||||
newQueue.unshift(this.current_song);
|
||||
newQueue = this.annotateQueue(newQueue);
|
||||
clearInterval(this.clockInterval);
|
||||
this.queue = newQueue;
|
||||
this.update_song_times();
|
||||
this.clockInterval = setInterval(this.update_song_times, 1000);
|
||||
|
||||
}).finally(() => {
|
||||
this.refreshing = false;
|
||||
this.refreshTimer = setTimeout(this.refresh, 10000);
|
||||
});
|
||||
}
|
||||
},
|
||||
annotateQueue(queue) {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const can_delete_previous = i === 0 || i === 1 ? false : queue[i - 1].can_delete;
|
||||
const previous_requested_by_user = i === 0 || i === 1 ? false : queue[i - 1].user !== null;
|
||||
const requested_by_marietje = queue[i].user === null;
|
||||
const next_is_marietje = i < queue.length - 1 && queue[i].user !== null && queue[i + 1].user === null;
|
||||
queue[i].can_delete = i !== 0 && (CAN_MOVE || (queue[i].user !== null && queue[i].user.id === this.user_data.id));
|
||||
queue[i].can_move_up = i !== 0 && ((CAN_MOVE && previous_requested_by_user && queue[i].user !== null) ||
|
||||
(CAN_MOVE && !previous_requested_by_user && queue[i].user === null) ||
|
||||
(can_delete_previous && !requested_by_marietje && previous_requested_by_user));
|
||||
queue[i].can_move_down = i !== 0 && ((CAN_MOVE && !next_is_marietje && i < queue.length - 1) ||
|
||||
(queue[i].can_delete && requested_by_marietje && next_is_marietje && i < queue.length - 1));
|
||||
queue[i].plays_at = null;
|
||||
queue[i].time_until_song_seconds = null;
|
||||
queue[i].played = false;
|
||||
}
|
||||
return queue;
|
||||
},
|
||||
cancel_song(id) {
|
||||
fetch(
|
||||
'/api/v1/queues/playlist-song/' + id + '/cancel/',
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}
|
||||
).finally(() => {
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
move_down(id) {
|
||||
fetch(
|
||||
'/api/v1/queues/playlist-song/' + id + '/move-down/',
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}
|
||||
).finally(() => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
const request_vue = new Vue({
|
||||
el: '#request-container',
|
||||
delimiters: ['<%', '%>'],
|
||||
data: {
|
||||
songs: [],
|
||||
total_songs: 0,
|
||||
search_input: "",
|
||||
typing_timer: null,
|
||||
page_size: 10,
|
||||
page_number: 1,
|
||||
},
|
||||
watch: {
|
||||
search_input: {
|
||||
handler(val, oldVal) {
|
||||
clearTimeout(this.typing_timer);
|
||||
if (this.search !== "") {
|
||||
this.typing_timer = setTimeout(this.search, 200);
|
||||
}
|
||||
}
|
||||
},
|
||||
page_number: {
|
||||
handler(val, oldVal) {
|
||||
if (this.page_number <= 0) {
|
||||
this.page_number = 1;
|
||||
}
|
||||
if (this.page_number > this.number_of_pages) {
|
||||
this.page_number = this.number_of_pages;
|
||||
}
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
page_size: {
|
||||
handler(val, oldVal) {
|
||||
if (this.page_size <= 0) {
|
||||
this.page_size = 10;
|
||||
}
|
||||
this.page_number = 1;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
number_of_pages: function () {
|
||||
return Math.ceil(this.total_songs / this.page_size);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
|
||||
).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(data => {
|
||||
this.songs = data.results;
|
||||
this.total_songs = data.count;
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
tata.error("", data.errorMessage);
|
||||
});
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
|
||||
{
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
}
|
||||
}
|
||||
).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(data => {
|
||||
this.songs = data.results;
|
||||
this.total_songs = data.count;
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
tata.error("", data.errorMessage);
|
||||
});
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
},
|
||||
request_song(song_id) {
|
||||
fetch('/api/v1/queues/current/request/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
song: song_id
|
||||
}),
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(() => {
|
||||
tata.success('', 'Song added to the queue.');
|
||||
queue_vue.refresh();
|
||||
}).catch(e => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
tata.error('', data.errorMessage);
|
||||
})
|
||||
} else {
|
||||
tata.error('', "An unknown exception occurred.")
|
||||
}
|
||||
});
|
||||
},
|
||||
report_song(song_id) {
|
||||
let message = prompt("What is wrong with the song?");
|
||||
if (message === null) {
|
||||
return;
|
||||
}
|
||||
if (message === "") {
|
||||
tata.error('', 'Please enter a message.');
|
||||
}
|
||||
fetch('/api/v1/songs/report-notes/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
song: song_id,
|
||||
note: message,
|
||||
}),
|
||||
}).then(response => {
|
||||
if (response.status === 201) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(() => {
|
||||
tata.success("", "Successfully submitted report note.")
|
||||
}).catch(e => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
tata.error("", data.errorMessage);
|
||||
});
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
},
|
||||
update_page(page_number) {
|
||||
this.page_number = page_number;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function volume_down() {
|
||||
fetch('/api/v1/queues/current/volume-down/', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw response;
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
tata.error("", e.errorMessage);
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred.")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function volume_up() {
|
||||
fetch('/api/v1/queues/current/volume-up/', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw response;
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
tata.error("", e.errorMessage);
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred.")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mute() {
|
||||
fetch('/api/v1/queues/current/mute/', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw response;
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
tata.error("", e.errorMessage);
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred.")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function skip() {
|
||||
fetch('/api/v1/queues/current/skip/', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw response;
|
||||
}
|
||||
}).then(() => {
|
||||
queue_vue.refresh();
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
tata.error("", e.errorMessage);
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred.")
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
app_name = 'queue'
|
||||
app_name = "queue"
|
||||
urlpatterns = []
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
return render(request, 'queues/queue.html')
|
||||
class QueueView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "queues/queue.html"
|
||||
|
||||
Reference in New Issue
Block a user