1 Commits

Author SHA1 Message Date
d55ff6c8c6 Change player to OAuth protocol 2023-11-12 09:33:19 +01:00
32 changed files with 442 additions and 614 deletions

View File

@ -1,4 +0,0 @@
marietje/db.sqlite3
marietje/static
docker-compose.yml.example
docker-compose.yml

View File

@ -16,8 +16,7 @@ black:
script:
- poetry run black --quiet --check marietje
# TODO: Fix the deploy stage, as it has not been adapted to the new server Marietje runs on. The . disables the stage.
.deploy:
deploy:
stage: deploy
only: ['marietje-zuid']
before_script:

View File

@ -1,26 +0,0 @@
FROM python:3.11
MAINTAINER Tartarus Technicie
ENV PYTHONUNBUFFERED 1
ENV DJANGO_SETTINGS_MODULE marietje.settings.production
ENV PATH /root/.poetry/bin:${PATH}
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
WORKDIR /marietje/src
COPY resources/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY poetry.lock pyproject.toml /marietje/src/
RUN \
mkdir --parents /marietje/src/ && \
mkdir --parents /marietje/log/ && \
mkdir --parents /marietje/static/ && \
chmod +x /usr/local/bin/entrypoint.sh && \
\
curl -sSL https://install.python-poetry.org | python3 - && \
export PATH="/root/.local/bin:$PATH" && \
poetry config --no-interaction --no-ansi virtualenvs.create false && \
poetry install --no-interaction --no-ansi --no-dev
COPY marietje /marietje/src/website/

View File

@ -1,43 +0,0 @@
services:
reverse-proxy:
container_name: 'marietje-reverse-proxy'
image: nginx:latest
restart: 'always'
depends_on:
- backend
ports:
- 80:80
volumes:
- ./data/shared/media/:/marietje/media/
- ./data/shared/static/:/marietje/static/
- ./data/reverse-proxy/conf.d/:/etc/nginx/conf.d/
- ./data/reverse-proxy/nginx.conf:/etc/nginx/nginx.conf
networks:
- marietje-network
backend:
build: "."
restart: 'always'
container_name: 'marietje-backend'
volumes:
- ./data/shared/static/:/marietje/src/website/static/
- ./data/shared/media/:/marietje/src/website/media/
- ./data/backend/log/:/marietje/log/
environment:
DJANGO_SECRET_KEY: '[Django Secret key]'
VIRTUAL_HOST: '[Marietje hostname]'
VIRTUAL_PROTO: 'uwsgi'
DJANGO_ALLOWED_HOST: 'marietje-zuid.nl'
DJANGO_MYSQL_NAME: 'marietje'
DJANGO_MYSQL_USER: 'marietje'
DJANGO_MYSQL_PASSWORD: '[Marietje zuid database password]'
DJANGO_MYSQL_HOST: 'localhost'
DJANGO_MYSQL_PORT: '3306'
DJANGO_BERTHA_HOST: 'bach.science.ru.nl'
DJANGO_BERTHA_PORT: '1234'
networks:
- marietje-network
networks:
marietje-network:
driver: bridge

View File

@ -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]

View File

@ -1,49 +0,0 @@
import os
from .base import *
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
DEBUG = False
ALLOWED_HOSTS = [os.environ.get("DJANGO_ALLOWED_HOST")]
SESSION_COOKIE_SECURE = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get("DJANGO_MYSQL_NAME"),
'USER': os.environ.get("DJANGO_MYSQL_USER"),
'PASSWORD': os.environ.get("DJANGO_MYSQL_PASSWORD"),
'HOST': os.environ.get("DJANGO_MYSQL_HOST"),
'PORT': os.environ.get("DJANGO_MYSQL_PORT"),
'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"},
}
}
# Logging
# https://docs.djangoproject.com/en/3.2/topics/logging/
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"file": {
"level": "INFO",
"class": "logging.FileHandler",
"filename": "/marietje/log/django.log",
},
},
"loggers": {
"": {
"handlers": ["file"],
"level": "DEBUG",
"propagate": True,
}, # noqa
}, # noqa
}
BASE_URL = 'https://marietje-zuid.science.ru.nl'
BERTHA_HOST = (os.environ.get("DJANGO_BERTHA_HOST"), os.environ.get("DJANGO_BERTHA_PORT"))

View File

@ -0,0 +1,23 @@
from .base import *
SECRET_KEY = '******'
DEBUG = False
ALLOWED_HOSTS = ['marietje-zuid.nl']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'marietje',
'USER': 'marietje',
'PASSWORD': '******',
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"},
}
}
BASE_URL = 'https://marietje-zuid.science.ru.nl'
BERTHA_HOST = ('bach.science.ru.nl', 1234)

View File

@ -31,6 +31,19 @@ a {
color: var(--text-color);
}
.table {
color: var(--text-color);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--background-shade);
color: var(--text-color);
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
color: var(--text-color);
}
input[type="text"], input[type="password"] {
background-color: var(--background-shade-light);
border: 1px solid var(--background-shade);
@ -53,23 +66,22 @@ button[type="button"] i {
min-width: 90px;
}
.song-info {
position: absolute;
padding: 8px;
background: silver;
white-space: nowrap;
z-index: 1;
}
#queue-time-header {
cursor: pointer;
}
footer {
text-align: center;
}
.table {
color: var(--text-color);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--background-shade);
color: var(--text-color);
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
color: var(--text-color);
}
.marietjequeue {
color: #777777;
}
@ -79,24 +91,7 @@ footer {
}
.marietjequeue-pre-start td {
border-bottom: 3px double var(--text-color);
}
.marietjequeue-post-start td {
border-top: 3px double var(--text-color);
}
.ownsong {
border-left: 1px solid var(--text-color);
}
.currentsong {
border-bottom: 1px solid var(--text-color);
font-weight: bold;
}
.underline_cell {
border-bottom: 1px solid var(--text-color);
border-bottom: 3px double #777777;
}
.block-button {
@ -105,10 +100,13 @@ footer {
transition: 1s transform ease-in-out;
}
.currentsong {
border-bottom: 1px solid #DDDDDD;
}
.navbar-text {
color: var(--text-color);
}
.danger {
color: red !important;
}

View File

@ -20,19 +20,17 @@
@media (prefers-color-scheme: dark) {
:root {
--background-color: #181818;
--background-shade: #282828;
--background-shade-light: #404040;
--background-color: #202020;
--background-shade: #404040;
--background-shade-light: #696969;
--card-background: #404040;
--card-background-shade: #282828;
--card-background-contrast: #dddddd;
--card-background: #696969;
--card-background-shade: #404040;
--card-background-contrast: #ffffff;
--title-color: #000000;
--sub-title-color: #dddddd;
--link-color: #007bff;
--text-color: #dddddd;
--bs-border-color: #282828;
--text-color: #ffffff;
}
}

View File

@ -15,10 +15,7 @@
<link href="{% static 'fontawesomefree/css/all.min.css' %}" rel="stylesheet" type="text/css">
<!-- Vue JS -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
</script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<!-- TaTa.js notifications -->
<script src="{% static 'marietje/js/tata.js' %}"></script>
@ -40,7 +37,6 @@
</section>
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand d-block d-lg-none" href="{% url "index" %}">Marietje 4.1</a>
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasNavbar" aria-controls="offcanvasNavbar">
<span class="navbar-toggler-icon"></span>

View File

@ -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")),
]

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class PlayerapiConfig(AppConfig):
name = "playerapi"

View File

@ -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

View File

@ -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),
]

View File

@ -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 marietje.utils import playlist_song_to_dict
from queues.models import Queue
from songs.models import Song
from .decorators import token_required
@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({})

View File

@ -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",
]

View File

@ -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"),
]

View File

@ -1,4 +1,13 @@
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
@ -6,9 +15,14 @@ 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
from queues.models import Playlist, PlaylistSong, QueueCommand, Queue
from queues.services import get_user_or_default_queue
from songs.counters import request_counter
from songs.models import Song
@ -47,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]
@ -80,12 +139,14 @@ class QueueSkipAPIView(APIView):
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")
):
playlist_song = queue.current_song()
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
@ -260,3 +321,41 @@ class QueueMuteAPIView(APIView):
return Response(status=403)
QueueCommand.objects.create(queue=queue, command="mute")
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)

View 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,
),
),
]

View File

@ -2,6 +2,7 @@ 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
@ -81,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()

View File

@ -5,7 +5,7 @@
{% block content %}
<nav class="navbar navbar-expand navbar-default navbar-light border-bottom">
<div class="container-lg">
<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"
@ -13,7 +13,7 @@
</button>
</li>
<li class="nav-item me-3" role="presentation">
<button onclick="request_vue.select_textinput()" class="nav-link" id="request-tab" data-bs-toggle="tab" data-bs-target="#request"
<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>
@ -39,32 +39,30 @@
</li>
</ul>
<ul id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<template v-if="infobar !== null && 'start_personal_queue' in infobar && infobar.start_personal_queue !== null">
<li v-if="infobar.start_personal_queue !== 0" class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts in ${ infobar.start_personal_queue.secondsToMMSS() }$
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts at ${ (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() }$
</p>
</li>
<li class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends in ${ infobar.end_personal_queue.secondsToMMSS() }$
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends at ${ (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() }$
</p>
</li>
<li class="nav-item">
<p class="navbar-text mb-0 duration-queue" v-bind:class="{danger: infobar.length_personal_queue > infobar.max_length * 60}">(${ infobar.length_personal_queue.secondsToMMSS() }$)</p>
</li>
</template>
<ul v-if="'start_personal_queue' in infobar && infobar.start_personal_queue !== null" id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<li v-if="infobar.start_personal_queue != 0" class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts in <% infobar.start_personal_queue.secondsToMMSS() %>
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts at <% (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() %>
</p>
</li>
<li class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends in <% infobar.end_personal_queue.secondsToMMSS() %>
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends at <% (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() %>
</p>
</li>
<li class="nav-item">
<p class="navbar-text mb-0 duration-queue" v-bind:class="{danger: infobar.length_personal_queue > infobar.max_length * 60}">(<% infobar.length_personal_queue.secondsToMMSS() %>)</p>
</li>
</ul>
</div>
</nav>
<div class="container-lg">
<div class="container">
<br><br>
<div class="alert-location">
</div>
@ -73,85 +71,53 @@
<div id="queue-container">
<table class="table table-striped">
<thead>
<tr class="table-header-style underline_cell">
<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" class="btn btn-link p-0" v-on:click="playsIn = false">Plays In</span>
<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">
<span class="control-icons">Control</span>
<span v-if="playsIn" class="btn btn-link p-0 d-sm-none" v-on:click="toggle_details(song)">(Plays in)</span>
<span v-else class="btn btn-link p-0 d-sm-none" v-on:click="toggle_details(song)">(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),
underline_cell: (index === queue[-1]),
currentsong: (index === 0),
ownsong: (this.user_data.id === song.user?.id && index !== 0),
}"
v-on:click="toggle_details(song)">
<td>
<span class="artist">${ song.song.artist }$</span>
<span v-if="show_details(song)" class="requested-by d-sm-none d-block small mt-3 fw-normal">
Requested by:<br>
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
${ song.user.name }$
</template>
</span>
</td>
<td>
<span class="title">${ song.song.title }$</span>
<span v-if="show_details(song) && song.time_until_song_seconds > 0" class="plays-at d-sm-none d-block small mt-3 fw-normal" style="text-align: right">
<span v-if="playsIn">Plays In:</span>
<span v-else>Plays At:</span>
<br>
<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>
</span>
</td>
<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 }$
<% 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() }$
<% song.time_until_song_seconds.secondsToMMSS() %>
</template>
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
${ song.plays_at.timestampToHHMMSS() }$
<% song.plays_at.timestampToHHMMSS() %>
</template>
</td>
<td>
<div class="d-flex flex-column">
<div class="d-flex flex-row">
<button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)"
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-arrow-up"></i></button>
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-arrow-up"></i></button>
<button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)" class="btn btn-link"><i
class="fa-solid fa-arrow-up"></i></button>
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-arrow-up"></i></button>
<button v-if="song.can_move_down" v-on:click="move_down(song.id)"
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-arrow-down"></i></button>
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-arrow-down"></i></button>
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)"
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
<button v-if="song.can_move_down" v-on:click="move_down(song.id)" class="btn btn-link"><i
class="fa-solid fa-arrow-down"></i></button>
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-arrow-down"></i></button>
</div>
<div class="d-flex flex-row">
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)" class="btn btn-link"><i
class="fa-solid fa-trash-can"></i></button>
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-trash-can"></i></button>
</div>
</div>
</td>
@ -162,7 +128,7 @@
</div>
</div>
<div class="tab-pane fade" id="request" role="tabpanel" aria-labelledby="request-tab">
<div id="request-container" class="table-responsive">
<div id="request-container">
<table id="request-table" class="table table-striped">
<thead>
<tr>
@ -173,7 +139,7 @@
<th>Report</th>
</tr>
<tr>
<th colspan="5"><input id="search-all" class="search-input" type="text" ref="search_textinput"
<th colspan="5"><input id="search-all" class="search-input" type="text"
v-model="search_input"/></th>
</tr>
</thead>
@ -210,7 +176,7 @@
</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>
<option :value="i"><% i %></option>
</template>
</select>
</th>
@ -221,21 +187,21 @@
<template v-for="(song, index) in songs">
<tr>
<td>
${ song.artist }$
<% song.artist %>
</td>
<td>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none">${ song.title }$</button>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none"><% song.title %></button>
</td>
<td>
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
${ song.user.name }$
<% song.user.name %>
</template>
</td>
<td>
${ song.duration.secondsToMMSS() }$
<% song.duration.secondsToMMSS() %>
</td>
<td>
<button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none">
@ -259,42 +225,21 @@
const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }};
</script>
<script>
const personal_queue_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
infobar: null,
}
},
}).mount('#personal-queue-container');
const queue_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
current_song: null,
queue: [],
user_data: null,
refreshing: true,
refreshTimer: null,
clockInterval: null,
started_at: null,
playsIn: true,
songs_show_details_on_mobile: [],
}
},
watch: {
playsIn: {
handler(val, oldVal) {
this.update_infobar();
setCookie("PLAYS_IN", this.playsIn, 14);
}
},
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);
const stored_playsIn = getCookie("PLAYS_IN");
this.playsIn = (stored_playsIn !== "false");
},
unmounted() {
clearInterval(this.clockInterval);
@ -357,14 +302,16 @@
plays_in: this.playsIn,
now_in_seconds: 0,
}
infoBar.now_in_seconds = Math.round((new Date()).getTime() / 1000);
const now_in_seconds = Math.round((new Date()).getTime() / 1000);
infoBar.now_in_seconds = now_in_seconds;
let current_song_played = now_in_seconds - this.queue[0].started_at;
// If the current song is the current user's, their queue has started.
if (this.queue[0].user.id === this.user_data.id) {
if (this.queue[0].user.id == this.user_data.id) {
infoBar.start_personal_queue = 0;
}
for (let i = 0; i < this.queue.length; i++) {
const current_song = this.queue[i];
if (i === 0) {
if (i == 0) {
const current_song_remaining_seconds = current_song.song.duration - this.queue[1].time_until_song_seconds;
infoBar['length_personal_queue'] -= current_song_remaining_seconds;
infoBar['length_total_queue'] -= current_song_remaining_seconds;
@ -374,11 +321,11 @@
infoBar['length_personal_queue'] += current_song.song.duration;
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
if (infoBar['start_personal_queue'] === null) {
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds;
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds
}
}
}
personal_queue_vue.infobar = infoBar;
this.$emit("infobar", infoBar);
},
refresh() {
if (!this.refreshing) {
@ -463,34 +410,30 @@
this.refresh();
});
},
show_details(song) {
return this.songs_show_details_on_mobile.includes(song.id);
},
toggle_details(song) {
if (!this.show_details(song)) {
this.songs_show_details_on_mobile.push(song.id);
} else {
// Deze filter is gehaat door Kees, gemaakt door Olaf. Bedankt, Olaf. Duurde wel even.
this.songs_show_details_on_mobile = this.songs_show_details_on_mobile.filter(
value => value !== song.id
);
}
},
}
}).mount("#queue-container");
});
const personal_queue_vue = new Vue({
el: '#personal-queue-container',
delimiters: ['<%', '%>'],
data: {
infobar: [],
},
mounted() {
queue_vue.$on("infobar", infoBar => this.infobar = infoBar);
}
});
</script>
<script>
const request_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
}
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: {
@ -582,7 +525,6 @@
}
});
},
request_song(song_id) {
fetch('/api/v1/queues/current/request/', {
method: 'POST',
@ -595,8 +537,7 @@
"Content-Type": 'application/json',
},
}).then(response => {
// TODO: Communicate failure through HTTP error codes (403) instead of checking response.success.
if (response.status === 200 && response.success) {
if (response.status === 200) {
return response.json();
} else {
throw response;
@ -614,7 +555,6 @@
}
});
},
report_song(song_id) {
let message = prompt("What is wrong with the song?");
if (message === null) {
@ -652,16 +592,11 @@
}
});
},
update_page(page_number) {
this.page_number = page_number;
},
select_textinput() {
this.$refs.search_textinput.select();
},
}
}
}).mount('#request-container');
});
</script>
<script>
function volume_down() {

View File

@ -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):

View File

@ -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"),
]

View File

@ -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):
@ -86,7 +111,7 @@ class SongUploadAPIView(APIView):
song = upload_file(file, artist, title, request.user)
upload_counter.inc()
return Response(status=200, data=self.serializer_class(song).data)
except (UploadException, ConnectionRefusedError):
except UploadException:
return Response(
status=500,
data={

View File

@ -4,7 +4,7 @@
{% block title %}Manage{% endblock %}
{% block content %}
<div class="container-lg">
<div class="container">
<div class="table-responsive mt-5">
<table id="request-table" class="table table-striped">
<thead>
@ -41,7 +41,7 @@
</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>
<option :value="i"><% i %></option>
</template>
</select>
</th>
@ -52,10 +52,10 @@
<template v-for="(song, index) in songs">
<tr>
<td>
${ song.artist }$
<% song.artist %>
</td>
<td>
<a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);">${ song.title }$</a>
<a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);"><% song.title %></a>
</td>
</tr>
</template>
@ -64,18 +64,17 @@
</div>
</div>
<script>
let manage_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
user_data: null,
}
let manage_vue = new Vue({
el: '#request-table',
delimiters: ['<%', '%>'],
data: {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
user_data: null,
},
watch: {
search_input: {
@ -168,6 +167,6 @@
this.page_number = page_number;
}
}
}).mount('#request-table');
});
</script>
{% endblock %}

View File

@ -16,12 +16,9 @@
{% csrf_token %}
<div class="fileupload fileupload-new" data-provides="fileupload">
<span class="btn btn-primary btn-file">
<span v-if="fileObjects.length === 0 && !files_loading">
<span v-if="fileObjects.length === 0">
Select files
</span>
<span v-else-if="files_loading">
Loading new files...
</span>
<span v-else>
Change
</span>
@ -32,35 +29,26 @@
<div class="songs">
<div v-for="fileObject in fileObjects" class="song-container card mb-3">
<div class="card-header">
<h3>${ fileObject.name }$</h3>
<h3><% fileObject.name %></h3>
</div>
<div class="card-body">
<div class="form-group mb-3">
<div v-if="fileObject.artist === '' || fileObject.artist === null"
class="alert alert-danger">Please enter an artist for this song.
</div>
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]"
class="form-control input-sm artist" disabled
<div v-if="fileObject.artist === '' || fileObject.artist === null" class="alert alert-danger">Please enter an artist for this song.</div>
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]" class="form-control input-sm artist" disabled
placeholder="Artist" v-model="fileObject.artist"/>
<input v-else type="text" name="artist[]"
class="form-control input-sm artist"
<input v-else type="text" name="artist[]" class="form-control input-sm artist"
placeholder="Artist" v-model="fileObject.artist"/>
</div>
<div class="form-group mb-3">
<div v-if="fileObject.title === '' || fileObject.title === null"
class="alert alert-danger">Please enter a title for this song.
</div>
<input v-if="upload_in_progress || uploaded" type="text" name="title[]"
class="form-control input-sm title" disabled
<div v-if="fileObject.title === '' || fileObject.title === null" class="alert alert-danger">Please enter a title for this song.</div>
<input v-if="upload_in_progress || uploaded" type="text" name="title[]" class="form-control input-sm title" disabled
placeholder="Title" v-model="fileObject.title"/>
<input v-else type="text" name="title[]" class="form-control input-sm title"
placeholder="Title" v-model="fileObject.title"/>
</div>
<template v-if="fileObject.upload_finished === true">
<div v-if="fileObject.success === true" class="alert alert-success">Upload
finished successfully.
</div>
<div v-else class="alert alert-danger">${ fileObject.error_message }$</div>
<div v-if="fileObject.success === true" class="alert alert-success">Upload finished successfully.</div>
<div v-else class="alert alert-danger"><% fileObject.error_message %></div>
</template>
</div>
</div>
@ -68,20 +56,14 @@
</div>
<div class="card-footer">
<div class="progress mt-2 mb-3">
<div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }"
class="progress-bar progress-bar-striped" role="progressbar"
:style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0"
aria-valuemax="100"></div>
<div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }" class="progress-bar progress-bar-striped" role="progressbar" :style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<template v-if="upload_in_progress || uploaded">
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">
Clear
</button>
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">Clear</button>
<button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button>
</template>
<template v-else>
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100"
type="submit" value="Upload" v-on:click="upload"/>
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100" type="submit" value="Upload" v-on:click="upload"/>
<button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button>
</template>
</div>
@ -91,21 +73,20 @@
</div>
</div>
<link rel="stylesheet" href="{% static 'songs/css/upload.css' %}"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
<script>
let upload_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
files: [],
fileObjects: [],
uploaded: false,
upload_in_progress: false,
files_loading: false,
}
<script type="module">
import * as id3 from '//unpkg.com/id3js@^2/lib/id3.js';
let upload_vue = new Vue({
el: '#uploadform',
delimiters: ['<%', '%>'],
data: {
files: [],
fileObjects: [],
uploaded: false,
upload_in_progress: false,
},
computed: {
ready_for_upload: function () {
ready_for_upload: function() {
if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) {
return false;
} else {
@ -117,14 +98,14 @@
return true;
}
},
everything_successfully_uploaded: function () {
everything_successfully_uploaded: function() {
return this.fileObjects.map((fileObject) => {
return fileObject.upload_finished === true && fileObject.success === true;
}).reduce((previousValue, currentValue) => {
return previousValue && currentValue;
}, true);
},
progress_bar_width: function () {
progress_bar_width: function() {
if (this.fileObjects.length === 0) {
return 0;
}
@ -177,20 +158,14 @@
}).then(() => {
this.fileObjects[i].success = true;
}).catch(e => {
console.log(e);
if (e instanceof Response) {
try {
e.json().then(data => {
this.fileObjects[i].error_message = data.errorMessage;
this.fileObjects[i].success = false;
});
} catch {
this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects[i].success = false;
}
e.json().then(data => {
this.fileObjects.error_message = data.errorMessage;
this.fileObjects.success = false;
});
} else {
this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects[i].success = false;
this.fileObjects.error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects.success = false;
}
}).finally(() => {
this.fileObjects[i].upload_finished = true;
@ -202,25 +177,23 @@
});
},
async set_new_files(event) {
this.files_loading = true;
this.uploaded = false;
this.upload_in_progress = false;
this.files = event.target.files;
let newFileObjects = [];
for (let i = 0; i < this.files.length; i++) {
await this.parseSong(this.files[i]).then((song) => {
try {
const tags = await this.parseSong(this.files[i]);
newFileObjects.push(
{
"file": this.files[i],
"name": this.files[i].name,
"artist": song.artist,
"title": song.title,
"artist": tags.artist,
"title": tags.title,
"success": null,
"error_message": null,
"upload_finished": false,
}
);
}).catch(() => {
} catch {
newFileObjects.push(
{
"file": this.files[i],
@ -232,28 +205,14 @@
"upload_finished": false,
}
)
});
}
}
this.fileObjects = newFileObjects;
this.files_loading = false;
},
async parseSong(file) {
let jsMediaTags = window.jsmediatags;
const tags = await new Promise((resolve, reject) => {
jsMediaTags.read(file, {
onSuccess: function (tag) {
resolve(tag);
},
onError: function (error) {
reject(error);
}
});
});
return tags.tags;
parseSong(file) {
return id3.fromFile(file);
}
}
}).mount('#uploadform');
});
</script>
{% endblock %}

View File

@ -28,7 +28,6 @@
<th>#</th>
<th>User</th>
<th style="text-align: right;"># Songs</th>
<th></th>
</tr>
</thead>
<tbody>
@ -55,7 +54,6 @@
<th>#</th>
<th>User</th>
<th style="text-align: right;"># Requests</th>
<th></th>
</tr>
</thead>
<tbody>
@ -111,7 +109,6 @@
<th>#</th>
<th>User</th>
<th style="text-align: right;"># Unique</th>
<th></th>
</tr>
</thead>
<tbody>
@ -179,9 +176,7 @@
</div>
<div class="col-md-6">
<h2>Most played uploaders</h2>
<p>These are the {{ stats.stats_top_count }} people whose songs are requested most often by other
people, as shown in the left column. The right column shows how many times that person has queued
their own songs.</p>
<p>These are the {{ stats.stats_top_count }} people whose songs are requested most often by other people, as shown in the left column. The right column shows how many times that person has queued his own songs.</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
@ -207,7 +202,7 @@
</div>
<div class="col-md-6">
<h2>Most played songs last 14 days</h2>
<p>These {{ stats.stats_top_count }} songs have been requested the most in the last two weeks.</p>
<p>These songs are played the {{ stats.stats_top_count }} most in the last two weeks.</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>

View File

@ -21,10 +21,9 @@
{% endif %}
<div class="col-md-6">
<h2>Most played songs</h2>
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different songs a total of
<strong> {{ stats.total_requests }} </strong> times. This means
<strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests
have been unique. These are the song you have requested the most.</p>
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different
songs a total of <strong> {{ stats.total_requests }} </strong> times. This
means <strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests have been unique. </p>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
@ -51,7 +50,6 @@
</div>
<div class="col-md-6">
<h2>Most played artists</h2>
<p>These are the artists you have requested the most.</p>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
@ -59,7 +57,7 @@
<tr>
<th>#</th>
<th>Artist</th>
<th style="white-space:nowrap; text-align: right;"># Requests</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
@ -76,11 +74,11 @@
</div>
<div class="col-md-6">
<h2>Uploads requested</h2>
<p>You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
shows how many times these have been requested by other people. The right column shows how many times
you requested your own songs. In total your songs have been queued
<strong> {{stats.total_played_uploads }} </strong> times by others and
<strong> {{stats.total_played_user_uploads }} </strong> times by yourself.</p>
<p> You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
shows how many times these have been requested by other people. The right column shows
how many times you requested your own songs. In total your songs
have been queued <strong> {{stats.total_played_uploads }} </strong> times by others and
<strong> {{stats.total_played_user_uploads }} </strong> by yourself.
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
@ -89,8 +87,8 @@
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th style="white-space:nowrap; text-align: right;"># Others</th>
<th style="white-space:nowrap;"># You</th>
<th style="text-align: right;">Others</th>
<th>You</th>
</tr>
</thead>
<tbody>
@ -109,8 +107,8 @@
</div>
<div class="col-md-6">
<h2>Upload artists requested</h2>
<p>The left column shows how many times songs from artists uploaded by you have been requested by
other people. The right column shows how many times you requested those songs.</p>
<p> The left column shows how many times songs from artists uploaded by you have been requested by
other people. The right column shows how many times you requested those songs.
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
@ -118,8 +116,8 @@
<tr>
<th>#</th>
<th>Artist</th>
<th style="white-space:nowrap; text-align: right;"># Others</th>
<th style="white-space:nowrap;"># You</th>
<th style="text-align: right;">Others</th>
<th>You</th>
</tr>
</thead>
<tbody>
@ -137,15 +135,14 @@
</div>
<div class="col-md-6">
<h2>Most played uploaders</h2>
<p>These are the people whose songs you have requested the most.</p>
<p> The people whose songs you have queued the most are:</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Uploader</th>
<th style="white-space:nowrap; text-align: right;"># Requests</th>
<th></th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>
@ -163,14 +160,14 @@
</div>
<div class="col-md-6">
<h2>Biggest fans</h2>
<p>These are the people that have requested your songs the most.</p>
<p> The people that queued your songs the most are:</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>User</th>
<th style="white-space:nowrap; text-align: right;"># Requests</th>
<th style="text-align: right;"># Requests</th>
</tr>
</thead>
<tbody>

View File

@ -1,5 +1,5 @@
[tool.poetry]
name = "marietje"
name = "MarietjeDjango"
version = "4.1.0"
description = "A music player for the south canteen of the Huygens building"
authors = [
@ -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"

View File

@ -1,31 +0,0 @@
#!/usr/bin/env bash
set -e
touch -a /marietje/log/uwsgi.log
touch -a /marietje/log/django.log
cd /marietje/src/website
./manage.py migrate --no-input
./manage.py collectstatic --no-input
chown --recursive www-data:www-data /marietje/
echo "Starting uwsgi server."
uwsgi --chdir=/marietje/src/website \
--module=marietje.wsgi:application \
--master --pidfile=/tmp/project-master.pid \
--socket=:8000 \
--processes=5 \
--uid=www-data --gid=www-data \
--harakiri=20 \
--post-buffering=16384 \
--max-requests=5000 \
--thunder-lock \
--vacuum \
--logfile-chown \
--logto2=/marietje/log/uwsgi.log \
--ignore-sigpipe \
--ignore-write-errors \
--disable-write-exception