mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2025-12-11 09:22:20 +01:00
Compare commits
1 Commits
44a1d44cd1
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
| 11e8ff11b8 |
@ -1,4 +0,0 @@
|
|||||||
marietje/db.sqlite3
|
|
||||||
marietje/static
|
|
||||||
docker-compose.yml.example
|
|
||||||
docker-compose.yml
|
|
||||||
@ -12,12 +12,11 @@ black:
|
|||||||
- python3 -m pip install --upgrade pip
|
- python3 -m pip install --upgrade pip
|
||||||
- curl -sSL https://install.python-poetry.org | python3 -
|
- curl -sSL https://install.python-poetry.org | python3 -
|
||||||
- export PATH="/root/.local/bin:$PATH"
|
- export PATH="/root/.local/bin:$PATH"
|
||||||
- poetry install --with dev
|
- poetry install --with dev --no-root
|
||||||
script:
|
script:
|
||||||
- poetry run black --quiet --check marietje
|
- 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
|
stage: deploy
|
||||||
only: ['marietje-zuid']
|
only: ['marietje-zuid']
|
||||||
before_script:
|
before_script:
|
||||||
|
|||||||
26
Dockerfile
26
Dockerfile
@ -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/
|
|
||||||
@ -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
|
|
||||||
@ -17,7 +17,7 @@ if __name__ == "__main__":
|
|||||||
# issue is really that Django is missing to avoid masking other
|
# issue is really that Django is missing to avoid masking other
|
||||||
# exceptions on Python 2.
|
# exceptions on Python 2.
|
||||||
try:
|
try:
|
||||||
import django
|
import django # noqa
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
|||||||
@ -7,13 +7,13 @@ from .models import User
|
|||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(BaseUserAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("username", "password", "queue")}),
|
(None, {"fields": ("username", "password")}),
|
||||||
(_("Personal info"), {"fields": ("name", "email")}),
|
(_("Personal info"), {"fields": ("name", "email")}),
|
||||||
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
|
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
|
||||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||||
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
|
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
|
||||||
)
|
)
|
||||||
list_display = ("username", "email", "name", "date_joined", "last_login", "queue", "is_staff")
|
list_display = ("username", "email", "name", "date_joined", "last_login", "is_staff")
|
||||||
search_fields = ("username", "name", "email")
|
search_fields = ("username", "name", "email")
|
||||||
|
|
||||||
def delete_model(self, request, user):
|
def delete_model(self, request, user):
|
||||||
|
|||||||
@ -28,7 +28,6 @@ class Command(BaseCommand):
|
|||||||
user.name = import_user["n"].strip()
|
user.name = import_user["n"].strip()
|
||||||
user.email = user.username + "@science.ru.nl"
|
user.email = user.username + "@science.ru.nl"
|
||||||
user.password = "md5$$" + import_user["p"]
|
user.password = "md5$$" + import_user["p"]
|
||||||
user.queue = get_first_queue()
|
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
if options["tsv_file"]:
|
if options["tsv_file"]:
|
||||||
@ -45,7 +44,6 @@ class Command(BaseCommand):
|
|||||||
user.name = import_user[2].decode("utf-8", errors="ignore").strip()
|
user.name = import_user[2].decode("utf-8", errors="ignore").strip()
|
||||||
user.email = user.username + "@science.ru.nl"
|
user.email = user.username + "@science.ru.nl"
|
||||||
user.password = import_user[3].decode("utf-8", errors="strict")
|
user.password = import_user[3].decode("utf-8", errors="strict")
|
||||||
user.queue = get_first_queue()
|
|
||||||
user.study = import_user[5].decode("utf-8", errors="ignore").strip()
|
user.study = import_user[5].decode("utf-8", errors="ignore").strip()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|||||||
28
marietje/marietje/migrations/0009_auto_20231124_2117.py
Normal file
28
marietje/marietje/migrations/0009_auto_20231124_2117.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-24 20:17
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_queue_mappings(apps, schema_editor):
|
||||||
|
"""Before removing the old reference to Queue from User, we should move this to the newly created model."""
|
||||||
|
User = apps.get_model("marietje", "User")
|
||||||
|
UserQueue = apps.get_model("queues", "UserQueue")
|
||||||
|
for user in User.objects.all():
|
||||||
|
if user.queue is not None:
|
||||||
|
UserQueue.objects.create(user=user, queue=user.queue)
|
||||||
|
else:
|
||||||
|
UserQueue.objects.create(user=user)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("marietje", "0008_alter_user_id"),
|
||||||
|
("queues", "0012_userqueue_queuelogentry"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
create_new_queue_mappings,
|
||||||
|
migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
||||||
16
marietje/marietje/migrations/0010_remove_user_queue.py
Normal file
16
marietje/marietje/migrations/0010_remove_user_queue.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-24 20:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("marietje", "0009_auto_20231124_2117"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="queue",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -7,9 +7,6 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from marietje.utils import get_first_queue
|
|
||||||
from queues.models import Queue
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
use_in_migrations = True
|
use_in_migrations = True
|
||||||
@ -19,9 +16,8 @@ class UserManager(BaseUserManager):
|
|||||||
raise ValueError("The given username must be set")
|
raise ValueError("The given username must be set")
|
||||||
email = self.normalize_email(email)
|
email = self.normalize_email(email)
|
||||||
username = self.model.normalize_username(username)
|
username = self.model.normalize_username(username)
|
||||||
queue = get_first_queue()
|
|
||||||
|
|
||||||
user = self.model(username=username, email=email, queue=queue, **extra_fields)
|
user = self.model(username=username, email=email, **extra_fields)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save(using=self._db)
|
user.save(using=self._db)
|
||||||
return user
|
return user
|
||||||
@ -80,8 +76,6 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
queue = models.ForeignKey(Queue, on_delete=models.SET_NULL, blank=True, null=True)
|
|
||||||
|
|
||||||
activation_token = models.TextField(_("activation token"), blank=True, null=True)
|
activation_token = models.TextField(_("activation token"), blank=True, null=True)
|
||||||
|
|
||||||
reset_token = models.TextField(_("reset token"), blank=True, null=True)
|
reset_token = models.TextField(_("reset token"), blank=True, null=True)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'marietje',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@ -15,7 +16,6 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework',
|
'rest_framework',
|
||||||
'tinymce',
|
'tinymce',
|
||||||
'announcements',
|
'announcements',
|
||||||
'marietje',
|
|
||||||
'queues',
|
'queues',
|
||||||
'songs',
|
'songs',
|
||||||
'stats',
|
'stats',
|
||||||
|
|||||||
@ -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"))
|
|
||||||
23
marietje/marietje/settings/production.py.example
Normal file
23
marietje/marietje/settings/production.py.example
Normal 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)
|
||||||
@ -31,6 +31,19 @@ a {
|
|||||||
color: var(--text-color);
|
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"] {
|
input[type="text"], input[type="password"] {
|
||||||
background-color: var(--background-shade-light);
|
background-color: var(--background-shade-light);
|
||||||
border: 1px solid var(--background-shade);
|
border: 1px solid var(--background-shade);
|
||||||
@ -53,23 +66,22 @@ button[type="button"] i {
|
|||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.song-info {
|
||||||
|
position: absolute;
|
||||||
|
padding: 8px;
|
||||||
|
background: silver;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queue-time-header {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
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 {
|
.marietjequeue {
|
||||||
color: #777777;
|
color: #777777;
|
||||||
}
|
}
|
||||||
@ -79,24 +91,7 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.marietjequeue-pre-start td {
|
.marietjequeue-pre-start td {
|
||||||
border-bottom: 3px double var(--text-color);
|
border-bottom: 3px double #777777;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-button {
|
.block-button {
|
||||||
@ -105,10 +100,13 @@ footer {
|
|||||||
transition: 1s transform ease-in-out;
|
transition: 1s transform ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.currentsong {
|
||||||
|
border-bottom: 1px solid #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-text {
|
.navbar-text {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
color: red !important;
|
color: red !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,19 +20,17 @@
|
|||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background-color: #181818;
|
--background-color: #202020;
|
||||||
--background-shade: #282828;
|
--background-shade: #404040;
|
||||||
--background-shade-light: #404040;
|
--background-shade-light: #696969;
|
||||||
|
|
||||||
--card-background: #404040;
|
--card-background: #696969;
|
||||||
--card-background-shade: #282828;
|
--card-background-shade: #404040;
|
||||||
--card-background-contrast: #dddddd;
|
--card-background-contrast: #ffffff;
|
||||||
|
|
||||||
--title-color: #000000;
|
--title-color: #000000;
|
||||||
--sub-title-color: #dddddd;
|
--sub-title-color: #dddddd;
|
||||||
--link-color: #007bff;
|
--link-color: #007bff;
|
||||||
--text-color: #dddddd;
|
--text-color: #ffffff;
|
||||||
|
|
||||||
--bs-border-color: #282828;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,10 +15,7 @@
|
|||||||
<link href="{% static 'fontawesomefree/css/all.min.css' %}" rel="stylesheet" type="text/css">
|
<link href="{% static 'fontawesomefree/css/all.min.css' %}" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
<!-- Vue JS -->
|
<!-- Vue JS -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
|
||||||
<script>
|
|
||||||
const { createApp } = Vue;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- TaTa.js notifications -->
|
<!-- TaTa.js notifications -->
|
||||||
<script src="{% static 'marietje/js/tata.js' %}"></script>
|
<script src="{% static 'marietje/js/tata.js' %}"></script>
|
||||||
@ -40,7 +37,6 @@
|
|||||||
</section>
|
</section>
|
||||||
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
|
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
|
||||||
<div class="container">
|
<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"
|
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="offcanvas"
|
||||||
data-bs-target="#offcanvasNavbar" aria-controls="offcanvasNavbar">
|
data-bs-target="#offcanvasNavbar" aria-controls="offcanvasNavbar">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
import binascii
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.http import StreamingHttpResponse
|
|
||||||
|
|
||||||
from queues.models import Queue, Playlist
|
|
||||||
|
|
||||||
|
|
||||||
def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options):
|
|
||||||
data = {
|
|
||||||
"id": song.id,
|
|
||||||
"artist": song.artist,
|
|
||||||
"title": song.title,
|
|
||||||
"duration": song.duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
if include_hash:
|
|
||||||
data["hash"] = song.hash
|
|
||||||
|
|
||||||
if include_user is not None and song.user is not None and song.user.name:
|
|
||||||
data["uploader_name"] = song.user.name
|
|
||||||
|
|
||||||
if include_replaygain:
|
|
||||||
data["rg_gain"] = song.rg_gain
|
|
||||||
data["rg_peak"] = song.rg_peak
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def playlist_song_to_dict(playlist_song, **options):
|
|
||||||
user = options.get("user")
|
|
||||||
return {
|
|
||||||
"id": playlist_song.id,
|
|
||||||
"requested_by": "Marietje" if playlist_song.user is None else playlist_song.user.name,
|
|
||||||
"song": song_to_dict(playlist_song.song, **options),
|
|
||||||
"can_move_down": playlist_song.user is not None and playlist_song.user == user,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Send a file to bertha file storage.
|
|
||||||
def send_to_bertha(file):
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.connect(settings.BERTHA_HOST)
|
|
||||||
sock.sendall(struct.pack("<BQ", 4, file.size))
|
|
||||||
|
|
||||||
for chunk in file.chunks():
|
|
||||||
sock.sendall(chunk)
|
|
||||||
sock.shutdown(socket.SHUT_WR)
|
|
||||||
song_hash = binascii.hexlify(sock.recv(64))
|
|
||||||
sock.close()
|
|
||||||
return song_hash
|
|
||||||
|
|
||||||
|
|
||||||
def get_first_queue():
|
|
||||||
queue = Queue.objects.first()
|
|
||||||
if queue is None:
|
|
||||||
playlist = Playlist()
|
|
||||||
playlist.save()
|
|
||||||
random_playlist = Playlist()
|
|
||||||
random_playlist.save()
|
|
||||||
queue = Queue(name="Queue", playlist=playlist, random_playlist=random_playlist)
|
|
||||||
queue.save()
|
|
||||||
return queue
|
|
||||||
|
|
||||||
|
|
||||||
def bertha_stream(song_hash):
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.connect(settings.BERTHA_HOST)
|
|
||||||
sock.sendall(struct.pack("<B", 2) + binascii.unhexlify(song_hash))
|
|
||||||
data = sock.recv(4096)
|
|
||||||
while data:
|
|
||||||
yield data
|
|
||||||
data = sock.recv(4096)
|
|
||||||
sock.close()
|
|
||||||
|
|
||||||
|
|
||||||
def get_from_bertha(song_hash):
|
|
||||||
response = StreamingHttpResponse(bertha_stream(song_hash))
|
|
||||||
response["Content-Disposition"] = 'attachment; filename="{}"'.format(song_hash)
|
|
||||||
return response
|
|
||||||
29
marietje/playerapi/services.py
Normal file
29
marietje/playerapi/services.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options):
|
||||||
|
data = {
|
||||||
|
"id": song.id,
|
||||||
|
"artist": song.artist,
|
||||||
|
"title": song.title,
|
||||||
|
"duration": song.duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
if include_hash:
|
||||||
|
data["hash"] = song.hash
|
||||||
|
|
||||||
|
if include_user is not None and song.user is not None and song.user.name:
|
||||||
|
data["uploader_name"] = song.user.name
|
||||||
|
|
||||||
|
if include_replaygain:
|
||||||
|
data["rg_gain"] = song.rg_gain
|
||||||
|
data["rg_peak"] = song.rg_peak
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_song_to_dict(playlist_song, **options):
|
||||||
|
user = options.get("user")
|
||||||
|
return {
|
||||||
|
"id": playlist_song.id,
|
||||||
|
"requested_by": "Marietje" if playlist_song.user is None else playlist_song.user.name,
|
||||||
|
"song": song_to_dict(playlist_song.song, **options),
|
||||||
|
"can_move_down": playlist_song.user is not None and playlist_song.user == user,
|
||||||
|
}
|
||||||
@ -4,11 +4,11 @@ from django.http import JsonResponse
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from marietje.utils import playlist_song_to_dict
|
|
||||||
from queues.models import Queue
|
from queues.models import Queue
|
||||||
from songs.models import Song
|
from songs.models import Song
|
||||||
|
|
||||||
from .decorators import token_required
|
from .decorators import token_required
|
||||||
|
from .services import playlist_song_to_dict
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
from django.contrib import admin
|
from typing import Optional
|
||||||
from .models import Queue, Playlist, PlaylistSong, QueueCommand
|
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from .models import Queue, Playlist, PlaylistSong, QueueCommand, QueueLogEntry, UserQueue
|
||||||
|
from marietje.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .services import get_queue_for_user
|
||||||
|
|
||||||
admin.site.register(Playlist)
|
admin.site.register(Playlist)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Queue)
|
@admin.register(Queue)
|
||||||
class OrderAdmin(admin.ModelAdmin):
|
class OrderAdmin(admin.ModelAdmin):
|
||||||
@ -21,3 +30,61 @@ class PlaylistSongAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
admin.site.register(QueueCommand)
|
admin.site.register(QueueCommand)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(QueueLogEntry)
|
||||||
|
class QueueLogEntryAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for log entries."""
|
||||||
|
|
||||||
|
list_display = [
|
||||||
|
"timestamp",
|
||||||
|
"queue",
|
||||||
|
"action",
|
||||||
|
"user",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
list_filter = [
|
||||||
|
"queue",
|
||||||
|
"action",
|
||||||
|
("timestamp", admin.DateFieldListFilter),
|
||||||
|
]
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
"""Disable delete permission."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
"""Disable add permission."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
"""Disable change permission."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class UserQueueInline(admin.StackedInline):
|
||||||
|
model = UserQueue
|
||||||
|
fields = ("queue",)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAdmin(BaseUserAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("username", "password")}),
|
||||||
|
(_("Personal info"), {"fields": ("name", "email")}),
|
||||||
|
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
|
||||||
|
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||||
|
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
|
||||||
|
)
|
||||||
|
list_display = ("username", "email", "name", "date_joined", "last_login", "queue__queue", "is_staff")
|
||||||
|
inlines = (UserQueueInline,)
|
||||||
|
|
||||||
|
def queue__queue(self, obj: User) -> Optional[Queue]:
|
||||||
|
"""Retrieve the Queue for a User."""
|
||||||
|
return get_queue_for_user(obj)
|
||||||
|
|
||||||
|
queue__queue.short_description = "queue"
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models import Q
|
||||||
from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404, CreateAPIView, DestroyAPIView
|
from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404, CreateAPIView, DestroyAPIView
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -8,7 +9,7 @@ from django.http import Http404
|
|||||||
|
|
||||||
from queues.api.v1.serializers import PlaylistSerializer, QueueSerializer, PlaylistSongSerializer
|
from queues.api.v1.serializers import PlaylistSerializer, QueueSerializer, PlaylistSongSerializer
|
||||||
from queues.exceptions import RequestException
|
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 queues.services import get_user_or_default_queue
|
||||||
from songs.counters import request_counter
|
from songs.counters import request_counter
|
||||||
from songs.models import Song
|
from songs.models import Song
|
||||||
@ -80,7 +81,7 @@ class QueueSkipAPIView(APIView):
|
|||||||
if queue is None:
|
if queue is None:
|
||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
|
|
||||||
playlist_song = request.user.queue.current_song()
|
playlist_song = queue.current_song()
|
||||||
if (
|
if (
|
||||||
request.user is not None
|
request.user is not None
|
||||||
and playlist_song.user != request.user
|
and playlist_song.user != request.user
|
||||||
@ -90,6 +91,7 @@ class QueueSkipAPIView(APIView):
|
|||||||
|
|
||||||
playlist_song.state = 2
|
playlist_song.state = 2
|
||||||
playlist_song.save()
|
playlist_song.save()
|
||||||
|
queue.log_action(request.user, "next", "Skipped to next song.")
|
||||||
|
|
||||||
return Response(status=200, data=QueueSerializer(queue).data)
|
return Response(status=200, data=QueueSerializer(queue).data)
|
||||||
|
|
||||||
@ -111,7 +113,18 @@ class PlaylistSongMoveDownAPIView(APIView):
|
|||||||
and not request.user.has_perm("queues.can_move")
|
and not request.user.has_perm("queues.can_move")
|
||||||
):
|
):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
|
|
||||||
playlist_song.move_down()
|
playlist_song.move_down()
|
||||||
|
|
||||||
|
for queue in Queue.objects.filter(
|
||||||
|
Q(playlist=playlist_song.playlist) | Q(random_playlist=playlist_song.playlist)
|
||||||
|
):
|
||||||
|
queue.log_action(
|
||||||
|
request.user,
|
||||||
|
"down",
|
||||||
|
'Moved song "{}" of playlist "{}" down.'.format(playlist_song.song, playlist_song.playlist),
|
||||||
|
)
|
||||||
|
|
||||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||||
|
|
||||||
|
|
||||||
@ -131,7 +144,18 @@ class PlaylistSongCancelAPIView(DestroyAPIView):
|
|||||||
and not request.user.has_perm("queues.can_cancel")
|
and not request.user.has_perm("queues.can_cancel")
|
||||||
):
|
):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
|
|
||||||
playlist_song.delete()
|
playlist_song.delete()
|
||||||
|
|
||||||
|
for queue in Queue.objects.filter(
|
||||||
|
Q(playlist=playlist_song.playlist) | Q(random_playlist=playlist_song.playlist)
|
||||||
|
):
|
||||||
|
queue.log_action(
|
||||||
|
request.user,
|
||||||
|
"cancel",
|
||||||
|
'Cancelled song "{}" of playlist "{}".'.format(playlist_song.song, playlist_song.playlist),
|
||||||
|
)
|
||||||
|
|
||||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||||
|
|
||||||
|
|
||||||
@ -165,6 +189,8 @@ class QueueRequestAPIView(CreateAPIView):
|
|||||||
except RequestException as e:
|
except RequestException as e:
|
||||||
return Response(data={"success": False, "errorMessage": str(e)})
|
return Response(data={"success": False, "errorMessage": str(e)})
|
||||||
|
|
||||||
|
queue.log_action(request.user, "request_song", "Requested song {}.".format(song))
|
||||||
|
|
||||||
request_counter.labels(queue=queue.name).inc()
|
request_counter.labels(queue=queue.name).inc()
|
||||||
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
return Response(status=200, data=self.serializer_class(playlist_song).data)
|
||||||
|
|
||||||
@ -196,7 +222,11 @@ class QueueVolumeDownAPIView(APIView):
|
|||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
|
|
||||||
QueueCommand.objects.create(queue=queue, command="volume_down")
|
QueueCommand.objects.create(queue=queue, command="volume_down")
|
||||||
|
|
||||||
|
queue.log_action(request.user, "volume_down", "Reduced the volume of {}.".format(queue))
|
||||||
|
|
||||||
return Response(status=200, data=self.serializer_class(queue).data)
|
return Response(status=200, data=self.serializer_class(queue).data)
|
||||||
|
|
||||||
|
|
||||||
@ -227,7 +257,11 @@ class QueueVolumeUpAPIView(APIView):
|
|||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
|
|
||||||
QueueCommand.objects.create(queue=queue, command="volume_up")
|
QueueCommand.objects.create(queue=queue, command="volume_up")
|
||||||
|
|
||||||
|
queue.log_action(request.user, "volume_up", "Increased the volume of {}.".format(queue))
|
||||||
|
|
||||||
return Response(status=200, data=self.serializer_class(queue).data)
|
return Response(status=200, data=self.serializer_class(queue).data)
|
||||||
|
|
||||||
|
|
||||||
@ -258,5 +292,9 @@ class QueueMuteAPIView(APIView):
|
|||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
|
|
||||||
QueueCommand.objects.create(queue=queue, command="mute")
|
QueueCommand.objects.create(queue=queue, command="mute")
|
||||||
|
|
||||||
|
queue.log_action(request.user, "mute", "Muted the volume of {}.".format(queue))
|
||||||
|
|
||||||
return Response(status=200, data=self.serializer_class(queue).data)
|
return Response(status=200, data=self.serializer_class(queue).data)
|
||||||
|
|||||||
64
marietje/queues/migrations/0012_userqueue_queuelogentry.py
Normal file
64
marietje/queues/migrations/0012_userqueue_queuelogentry.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-24 20:17
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("queues", "0011_alter_playlistsong_playlist"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserQueue",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
(
|
||||||
|
"queue",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="users",
|
||||||
|
to="queues.queue",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="queue_new",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="QueueLogEntry",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("action", models.CharField(max_length=255)),
|
||||||
|
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("description", models.CharField(max_length=255)),
|
||||||
|
(
|
||||||
|
"queue",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name="logs", to="queues.queue"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "player log entry",
|
||||||
|
"verbose_name_plural": "player log entries",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
22
marietje/queues/migrations/0013_alter_userqueue_user.py
Normal file
22
marietje/queues/migrations/0013_alter_userqueue_user.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-24 20:19
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("queues", "0012_userqueue_queuelogentry"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userqueue",
|
||||||
|
name="user",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name="queue", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -6,6 +7,8 @@ from django.utils import timezone
|
|||||||
from queues.exceptions import RequestException
|
from queues.exceptions import RequestException
|
||||||
from songs.models import Song
|
from songs.models import Song
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class Playlist(models.Model):
|
class Playlist(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -143,10 +146,41 @@ class Queue(models.Model):
|
|||||||
playlist_song.save()
|
playlist_song.save()
|
||||||
song_count += 1
|
song_count += 1
|
||||||
|
|
||||||
|
def log_action(self, user: User, action: str, description: str) -> "QueueLogEntry":
|
||||||
|
"""
|
||||||
|
Log a queue action.
|
||||||
|
|
||||||
|
:param user: The user performing the action.
|
||||||
|
:param action: An identifier of the action performed.
|
||||||
|
:param description: An optional description for the action.
|
||||||
|
:return: The created QueueLogEntry object.
|
||||||
|
"""
|
||||||
|
return QueueLogEntry.objects.create(
|
||||||
|
queue=self,
|
||||||
|
user=user,
|
||||||
|
action=action,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class UserQueue(models.Model):
|
||||||
|
"""
|
||||||
|
UserQueue model.
|
||||||
|
|
||||||
|
This model connects a user to its queue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="queue")
|
||||||
|
queue = models.ForeignKey(Queue, on_delete=models.SET_NULL, null=True, blank=True, related_name="users")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Convert this object to string."""
|
||||||
|
return "Queue for user {}".format(self.user)
|
||||||
|
|
||||||
|
|
||||||
class QueueCommand(models.Model):
|
class QueueCommand(models.Model):
|
||||||
queue = models.ForeignKey(
|
queue = models.ForeignKey(
|
||||||
Queue,
|
Queue,
|
||||||
@ -157,3 +191,20 @@ class QueueCommand(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.command)
|
return str(self.command)
|
||||||
|
|
||||||
|
|
||||||
|
class QueueLogEntry(models.Model):
|
||||||
|
"""Model for logging queue events."""
|
||||||
|
|
||||||
|
queue = models.ForeignKey(Queue, on_delete=models.CASCADE, related_name="logs")
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
action = models.CharField(max_length=255)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
description = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.queue} {self.action} by {self.user} at {self.timestamp}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "player log entry"
|
||||||
|
verbose_name_plural = "player log entries"
|
||||||
|
|||||||
@ -1,15 +1,44 @@
|
|||||||
from queues.models import Queue
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from queues.models import Queue, Playlist
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
def get_user_or_default_queue(request):
|
User = get_user_model()
|
||||||
"""Get the user or default queue."""
|
|
||||||
|
|
||||||
|
def get_user_or_default_queue(request) -> Queue:
|
||||||
|
"""Get the user or default queue from a request."""
|
||||||
if request.user is None:
|
if request.user is None:
|
||||||
return get_default_queue()
|
return get_default_queue()
|
||||||
else:
|
else:
|
||||||
return request.user.queue
|
return get_queue_for_user(request.user)
|
||||||
|
|
||||||
|
|
||||||
def get_default_queue():
|
def get_queue_for_user(user: User) -> Optional[Queue]:
|
||||||
|
"""Get the queue for a User."""
|
||||||
|
if user.queue is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return user.queue.queue
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_queue() -> Queue:
|
||||||
"""Get the default queue."""
|
"""Get the default queue."""
|
||||||
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)
|
try:
|
||||||
|
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)
|
||||||
|
except Queue.DoesNotExist:
|
||||||
|
return get_first_queue()
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_queue() -> Queue:
|
||||||
|
"""Get the first Queue object or create one."""
|
||||||
|
queue = Queue.objects.first()
|
||||||
|
if queue is not None:
|
||||||
|
return queue
|
||||||
|
|
||||||
|
playlist = Playlist.objects.create()
|
||||||
|
random_playlist = Playlist.objects.create()
|
||||||
|
return Queue.objects.create(name="Queue", playlist=playlist, random_playlist=random_playlist)
|
||||||
|
|||||||
18
marietje/queues/signals.py
Normal file
18
marietje/queues/signals.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from queues.models import UserQueue
|
||||||
|
from queues.services import get_default_queue
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def create_default_queue(sender, instance, created, **kwargs):
|
||||||
|
"""Create a UserQueue object when a User gets created."""
|
||||||
|
if created:
|
||||||
|
user_queue, user_queue_created = UserQueue.objects.get_or_create(user=instance)
|
||||||
|
if user_queue_created:
|
||||||
|
user_queue.queue = get_default_queue()
|
||||||
|
user_queue.save()
|
||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav class="navbar navbar-expand navbar-default navbar-light border-bottom">
|
<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">
|
<ul class="nav nav-pills" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="queue-tab" data-bs-toggle="tab" data-bs-target="#queue"
|
<button class="nav-link active" id="queue-tab" data-bs-toggle="tab" data-bs-target="#queue"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item me-3" role="presentation">
|
<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
|
type="button" role="tab" aria-controls="request" aria-selected="false">Request
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -39,32 +39,30 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
|
<ul v-if="'start_personal_queue' in infobar && infobar.start_personal_queue !== null" 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">
|
||||||
<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">
|
||||||
<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() %>
|
||||||
First song starts in ${ infobar.start_personal_queue.secondsToMMSS() }$
|
</p>
|
||||||
</p>
|
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
|
||||||
<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() %>
|
||||||
First song starts at ${ (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() }$
|
</p>
|
||||||
</p>
|
</li>
|
||||||
</li>
|
<li class="nav-item me-3">
|
||||||
<li class="nav-item me-3">
|
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
|
||||||
<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() %>
|
||||||
Last song ends in ${ infobar.end_personal_queue.secondsToMMSS() }$
|
</p>
|
||||||
</p>
|
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
|
||||||
<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() %>
|
||||||
Last song ends at ${ (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() }$
|
</p>
|
||||||
</p>
|
</li>
|
||||||
</li>
|
<li class="nav-item">
|
||||||
<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>
|
||||||
<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>
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container-lg">
|
<div class="container">
|
||||||
<br><br>
|
<br><br>
|
||||||
<div class="alert-location">
|
<div class="alert-location">
|
||||||
</div>
|
</div>
|
||||||
@ -73,85 +71,53 @@
|
|||||||
<div id="queue-container">
|
<div id="queue-container">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<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">Artist</td>
|
||||||
<td class="col-md-4">Title</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-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;">
|
<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>
|
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays At</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-md-1">
|
<td class="col-md-1 control-icons">Control</td>
|
||||||
<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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="queuebody">
|
<tbody class="queuebody">
|
||||||
<template v-for="(song, index) in queue">
|
<template v-for="(song, index) in queue">
|
||||||
<tr :class="{ marietjequeue: (song.user === null),
|
<tr :class="{ marietjequeue: (song.user === null), currentsong: (index === 0), 'fw-bold': (index === 0) }">
|
||||||
underline_cell: (index === queue[-1]),
|
<td class="artist"><% song.song.artist %></td>
|
||||||
currentsong: (index === 0),
|
<td class="title"><% song.song.title %></td>
|
||||||
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>
|
|
||||||
<td class="d-sm-table-cell d-none requested-by">
|
<td class="d-sm-table-cell d-none requested-by">
|
||||||
<template v-if="song.user === null">
|
<template v-if="song.user === null">
|
||||||
Marietje
|
Marietje
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
${ song.user.name }$
|
<% song.user.name %>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-sm-table-cell d-none plays-at" style="text-align: right">
|
<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">
|
<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>
|
||||||
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
|
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
|
||||||
${ song.plays_at.timestampToHHMMSS() }$
|
<% song.plays_at.timestampToHHMMSS() %>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)"
|
<button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)" class="btn btn-link"><i
|
||||||
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-arrow-up"></i></button>
|
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-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)"
|
<button v-if="song.can_move_down" v-on:click="move_down(song.id)" class="btn btn-link"><i
|
||||||
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-arrow-down"></i></button>
|
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-else class="btn btn-link invisible"><i class="fa-solid fa-arrow-down"></i></button>
|
||||||
|
</div>
|
||||||
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)"
|
<div class="d-flex flex-row">
|
||||||
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
|
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)" class="btn btn-link"><i
|
||||||
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -162,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="request" role="tabpanel" aria-labelledby="request-tab">
|
<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">
|
<table id="request-table" class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -173,7 +139,7 @@
|
|||||||
<th>Report</th>
|
<th>Report</th>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
v-model="search_input"/></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -210,7 +176,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
|
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
|
||||||
<template v-for="(i, index) in number_of_pages">
|
<template v-for="(i, index) in number_of_pages">
|
||||||
<option :value="i">${ i }$</option>
|
<option :value="i"><% i %></option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</th>
|
</th>
|
||||||
@ -221,21 +187,21 @@
|
|||||||
<template v-for="(song, index) in songs">
|
<template v-for="(song, index) in songs">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
${ song.artist }$
|
<% song.artist %>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="song.user === null">
|
<template v-if="song.user === null">
|
||||||
Marietje
|
Marietje
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
${ song.user.name }$
|
<% song.user.name %>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
${ song.duration.secondsToMMSS() }$
|
<% song.duration.secondsToMMSS() %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none">
|
<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" }};
|
const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }};
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
const personal_queue_vue = createApp({
|
const queue_vue = new Vue({
|
||||||
delimiters: ['${', '}$'],
|
el: '#queue-container',
|
||||||
data() {
|
delimiters: ['<%', '%>'],
|
||||||
return {
|
data: {
|
||||||
infobar: null,
|
current_song: null,
|
||||||
}
|
queue: [],
|
||||||
},
|
user_data: null,
|
||||||
}).mount('#personal-queue-container');
|
refreshing: true,
|
||||||
const queue_vue = createApp({
|
refreshTimer: null,
|
||||||
delimiters: ['${', '}$'],
|
clockInterval: null,
|
||||||
data() {
|
started_at: null,
|
||||||
return {
|
playsIn: true,
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.clockInterval = setInterval(this.update_song_times, 1000);
|
this.clockInterval = setInterval(this.update_song_times, 1000);
|
||||||
|
|
||||||
const stored_playsIn = getCookie("PLAYS_IN");
|
|
||||||
this.playsIn = (stored_playsIn !== "false");
|
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
clearInterval(this.clockInterval);
|
clearInterval(this.clockInterval);
|
||||||
@ -357,14 +302,16 @@
|
|||||||
plays_in: this.playsIn,
|
plays_in: this.playsIn,
|
||||||
now_in_seconds: 0,
|
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 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;
|
infoBar.start_personal_queue = 0;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.queue.length; i++) {
|
for (let i = 0; i < this.queue.length; i++) {
|
||||||
const current_song = this.queue[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;
|
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_personal_queue'] -= current_song_remaining_seconds;
|
||||||
infoBar['length_total_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['length_personal_queue'] += current_song.song.duration;
|
||||||
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
|
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
|
||||||
if (infoBar['start_personal_queue'] === null) {
|
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() {
|
refresh() {
|
||||||
if (!this.refreshing) {
|
if (!this.refreshing) {
|
||||||
@ -463,34 +410,30 @@
|
|||||||
this.refresh();
|
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>
|
||||||
<script>
|
<script>
|
||||||
const request_vue = createApp({
|
const request_vue = new Vue({
|
||||||
delimiters: ['${', '}$'],
|
el: '#request-container',
|
||||||
data() {
|
delimiters: ['<%', '%>'],
|
||||||
return {
|
data: {
|
||||||
songs: [],
|
songs: [],
|
||||||
total_songs: 0,
|
total_songs: 0,
|
||||||
search_input: "",
|
search_input: "",
|
||||||
typing_timer: null,
|
typing_timer: null,
|
||||||
page_size: 10,
|
page_size: 10,
|
||||||
page_number: 1,
|
page_number: 1,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search_input: {
|
search_input: {
|
||||||
@ -582,7 +525,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
request_song(song_id) {
|
request_song(song_id) {
|
||||||
fetch('/api/v1/queues/current/request/', {
|
fetch('/api/v1/queues/current/request/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -595,8 +537,7 @@
|
|||||||
"Content-Type": 'application/json',
|
"Content-Type": 'application/json',
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
// TODO: Communicate failure through HTTP error codes (403) instead of checking response.success.
|
if (response.status === 200) {
|
||||||
if (response.status === 200 && response.success) {
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} else {
|
} else {
|
||||||
throw response;
|
throw response;
|
||||||
@ -614,7 +555,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
report_song(song_id) {
|
report_song(song_id) {
|
||||||
let message = prompt("What is wrong with the song?");
|
let message = prompt("What is wrong with the song?");
|
||||||
if (message === null) {
|
if (message === null) {
|
||||||
@ -652,16 +592,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
update_page(page_number) {
|
update_page(page_number) {
|
||||||
this.page_number = page_number;
|
this.page_number = page_number;
|
||||||
},
|
}
|
||||||
|
|
||||||
select_textinput() {
|
|
||||||
this.$refs.search_textinput.select();
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}).mount('#request-container');
|
});
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
function volume_down() {
|
function volume_down() {
|
||||||
|
|||||||
@ -86,7 +86,7 @@ class SongUploadAPIView(APIView):
|
|||||||
song = upload_file(file, artist, title, request.user)
|
song = upload_file(file, artist, title, request.user)
|
||||||
upload_counter.inc()
|
upload_counter.inc()
|
||||||
return Response(status=200, data=self.serializer_class(song).data)
|
return Response(status=200, data=self.serializer_class(song).data)
|
||||||
except (UploadException, ConnectionRefusedError):
|
except UploadException:
|
||||||
return Response(
|
return Response(
|
||||||
status=500,
|
status=500,
|
||||||
data={
|
data={
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
from marietje.utils import send_to_bertha
|
import binascii
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from queues.models import PlaylistSong
|
from queues.models import PlaylistSong
|
||||||
from songs.models import Song
|
from songs.models import Song
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
@ -11,6 +16,20 @@ class UploadException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def send_to_bertha(file):
|
||||||
|
"""Send a file to Berthad file storage."""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect(settings.BERTHA_HOST)
|
||||||
|
sock.sendall(struct.pack("<BQ", 4, file.size))
|
||||||
|
|
||||||
|
for chunk in file.chunks():
|
||||||
|
sock.sendall(chunk)
|
||||||
|
sock.shutdown(socket.SHUT_WR)
|
||||||
|
song_hash = binascii.hexlify(sock.recv(64))
|
||||||
|
sock.close()
|
||||||
|
return song_hash
|
||||||
|
|
||||||
|
|
||||||
def is_regular_queue(ps):
|
def is_regular_queue(ps):
|
||||||
if not ps.played_at:
|
if not ps.played_at:
|
||||||
# Request is from the old times, assume good
|
# Request is from the old times, assume good
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{% block title %}Manage{% endblock %}
|
{% block title %}Manage{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-lg">
|
<div class="container">
|
||||||
<div class="table-responsive mt-5">
|
<div class="table-responsive mt-5">
|
||||||
<table id="request-table" class="table table-striped">
|
<table id="request-table" class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
|
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
|
||||||
<template v-for="(i, index) in number_of_pages">
|
<template v-for="(i, index) in number_of_pages">
|
||||||
<option :value="i">${ i }$</option>
|
<option :value="i"><% i %></option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</th>
|
</th>
|
||||||
@ -52,10 +52,10 @@
|
|||||||
<template v-for="(song, index) in songs">
|
<template v-for="(song, index) in songs">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
${ song.artist }$
|
<% song.artist %>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -64,18 +64,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
let manage_vue = createApp({
|
let manage_vue = new Vue({
|
||||||
delimiters: ['${', '}$'],
|
el: '#request-table',
|
||||||
data() {
|
delimiters: ['<%', '%>'],
|
||||||
return {
|
data: {
|
||||||
songs: [],
|
songs: [],
|
||||||
total_songs: 0,
|
total_songs: 0,
|
||||||
search_input: "",
|
search_input: "",
|
||||||
typing_timer: null,
|
typing_timer: null,
|
||||||
page_size: 10,
|
page_size: 10,
|
||||||
page_number: 1,
|
page_number: 1,
|
||||||
user_data: null,
|
user_data: null,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search_input: {
|
search_input: {
|
||||||
@ -168,6 +167,6 @@
|
|||||||
this.page_number = page_number;
|
this.page_number = page_number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).mount('#request-table');
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -16,12 +16,9 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="fileupload fileupload-new" data-provides="fileupload">
|
<div class="fileupload fileupload-new" data-provides="fileupload">
|
||||||
<span class="btn btn-primary btn-file">
|
<span class="btn btn-primary btn-file">
|
||||||
<span v-if="fileObjects.length === 0 && !files_loading">
|
<span v-if="fileObjects.length === 0">
|
||||||
Select files
|
Select files
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="files_loading">
|
|
||||||
Loading new files...
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Change
|
Change
|
||||||
</span>
|
</span>
|
||||||
@ -32,35 +29,26 @@
|
|||||||
<div class="songs">
|
<div class="songs">
|
||||||
<div v-for="fileObject in fileObjects" class="song-container card mb-3">
|
<div v-for="fileObject in fileObjects" class="song-container card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>${ fileObject.name }$</h3>
|
<h3><% fileObject.name %></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<div v-if="fileObject.artist === '' || fileObject.artist === null"
|
<div v-if="fileObject.artist === '' || fileObject.artist === null" class="alert alert-danger">Please enter an artist for this song.</div>
|
||||||
class="alert alert-danger">Please enter an artist for this song.
|
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]" class="form-control input-sm artist" disabled
|
||||||
</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"/>
|
placeholder="Artist" v-model="fileObject.artist"/>
|
||||||
<input v-else type="text" name="artist[]"
|
<input v-else type="text" name="artist[]" class="form-control input-sm artist"
|
||||||
class="form-control input-sm artist"
|
|
||||||
placeholder="Artist" v-model="fileObject.artist"/>
|
placeholder="Artist" v-model="fileObject.artist"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<div v-if="fileObject.title === '' || fileObject.title === null"
|
<div v-if="fileObject.title === '' || fileObject.title === null" class="alert alert-danger">Please enter a title for this song.</div>
|
||||||
class="alert alert-danger">Please enter a title for this song.
|
<input v-if="upload_in_progress || uploaded" type="text" name="title[]" class="form-control input-sm title" disabled
|
||||||
</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"/>
|
placeholder="Title" v-model="fileObject.title"/>
|
||||||
<input v-else type="text" name="title[]" class="form-control input-sm title"
|
<input v-else type="text" name="title[]" class="form-control input-sm title"
|
||||||
placeholder="Title" v-model="fileObject.title"/>
|
placeholder="Title" v-model="fileObject.title"/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="fileObject.upload_finished === true">
|
<template v-if="fileObject.upload_finished === true">
|
||||||
<div v-if="fileObject.success === true" class="alert alert-success">Upload
|
<div v-if="fileObject.success === true" class="alert alert-success">Upload finished successfully.</div>
|
||||||
finished successfully.
|
<div v-else class="alert alert-danger"><% fileObject.error_message %></div>
|
||||||
</div>
|
|
||||||
<div v-else class="alert alert-danger">${ fileObject.error_message }$</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,20 +56,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="progress mt-2 mb-3">
|
<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) }"
|
<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>
|
||||||
class="progress-bar progress-bar-striped" role="progressbar"
|
|
||||||
:style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0"
|
|
||||||
aria-valuemax="100"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="upload_in_progress || uploaded">
|
<template v-if="upload_in_progress || uploaded">
|
||||||
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">
|
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">Clear</button>
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button>
|
<button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100"
|
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100" type="submit" value="Upload" v-on:click="upload"/>
|
||||||
type="submit" value="Upload" v-on:click="upload"/>
|
|
||||||
<button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button>
|
<button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -91,21 +73,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<link rel="stylesheet" href="{% static 'songs/css/upload.css' %}"/>
|
<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 type="module">
|
||||||
<script>
|
import * as id3 from '//unpkg.com/id3js@^2/lib/id3.js';
|
||||||
let upload_vue = createApp({
|
|
||||||
delimiters: ['${', '}$'],
|
let upload_vue = new Vue({
|
||||||
data() {
|
el: '#uploadform',
|
||||||
return {
|
delimiters: ['<%', '%>'],
|
||||||
files: [],
|
data: {
|
||||||
fileObjects: [],
|
files: [],
|
||||||
uploaded: false,
|
fileObjects: [],
|
||||||
upload_in_progress: false,
|
uploaded: false,
|
||||||
files_loading: false,
|
upload_in_progress: false,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ready_for_upload: function () {
|
ready_for_upload: function() {
|
||||||
if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) {
|
if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@ -117,14 +98,14 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
everything_successfully_uploaded: function () {
|
everything_successfully_uploaded: function() {
|
||||||
return this.fileObjects.map((fileObject) => {
|
return this.fileObjects.map((fileObject) => {
|
||||||
return fileObject.upload_finished === true && fileObject.success === true;
|
return fileObject.upload_finished === true && fileObject.success === true;
|
||||||
}).reduce((previousValue, currentValue) => {
|
}).reduce((previousValue, currentValue) => {
|
||||||
return previousValue && currentValue;
|
return previousValue && currentValue;
|
||||||
}, true);
|
}, true);
|
||||||
},
|
},
|
||||||
progress_bar_width: function () {
|
progress_bar_width: function() {
|
||||||
if (this.fileObjects.length === 0) {
|
if (this.fileObjects.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -177,20 +158,14 @@
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.fileObjects[i].success = true;
|
this.fileObjects[i].success = true;
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.log(e);
|
|
||||||
if (e instanceof Response) {
|
if (e instanceof Response) {
|
||||||
try {
|
e.json().then(data => {
|
||||||
e.json().then(data => {
|
this.fileObjects.error_message = data.errorMessage;
|
||||||
this.fileObjects[i].error_message = data.errorMessage;
|
this.fileObjects.success = false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
|
this.fileObjects.error_message = "An exception occurred while uploading this file, please try again.";
|
||||||
this.fileObjects[i].success = false;
|
this.fileObjects.success = false;
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.fileObjects[i].upload_finished = true;
|
this.fileObjects[i].upload_finished = true;
|
||||||
@ -202,25 +177,23 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
async set_new_files(event) {
|
async set_new_files(event) {
|
||||||
this.files_loading = true;
|
|
||||||
this.uploaded = false;
|
|
||||||
this.upload_in_progress = false;
|
|
||||||
this.files = event.target.files;
|
this.files = event.target.files;
|
||||||
let newFileObjects = [];
|
let newFileObjects = [];
|
||||||
for (let i = 0; i < this.files.length; i++) {
|
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(
|
newFileObjects.push(
|
||||||
{
|
{
|
||||||
"file": this.files[i],
|
"file": this.files[i],
|
||||||
"name": this.files[i].name,
|
"name": this.files[i].name,
|
||||||
"artist": song.artist,
|
"artist": tags.artist,
|
||||||
"title": song.title,
|
"title": tags.title,
|
||||||
"success": null,
|
"success": null,
|
||||||
"error_message": null,
|
"error_message": null,
|
||||||
"upload_finished": false,
|
"upload_finished": false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}).catch(() => {
|
} catch {
|
||||||
newFileObjects.push(
|
newFileObjects.push(
|
||||||
{
|
{
|
||||||
"file": this.files[i],
|
"file": this.files[i],
|
||||||
@ -232,28 +205,14 @@
|
|||||||
"upload_finished": false,
|
"upload_finished": false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
this.fileObjects = newFileObjects;
|
this.fileObjects = newFileObjects;
|
||||||
this.files_loading = false;
|
|
||||||
},
|
},
|
||||||
async parseSong(file) {
|
parseSong(file) {
|
||||||
let jsMediaTags = window.jsmediatags;
|
return id3.fromFile(file);
|
||||||
|
|
||||||
const tags = await new Promise((resolve, reject) => {
|
|
||||||
jsMediaTags.read(file, {
|
|
||||||
onSuccess: function (tag) {
|
|
||||||
resolve(tag);
|
|
||||||
},
|
|
||||||
onError: function (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return tags.tags;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).mount('#uploadform');
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -28,7 +28,6 @@
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th style="text-align: right;"># Songs</th>
|
<th style="text-align: right;"># Songs</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -55,7 +54,6 @@
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th style="text-align: right;"># Requests</th>
|
<th style="text-align: right;"># Requests</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -111,7 +109,6 @@
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th style="text-align: right;"># Unique</th>
|
<th style="text-align: right;"># Unique</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -179,9 +176,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Most played uploaders</h2>
|
<h2>Most played uploaders</h2>
|
||||||
<p>These are the {{ stats.stats_top_count }} people whose songs are requested most often by other
|
<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>
|
||||||
people, as shown in the left column. The right column shows how many times that person has queued
|
|
||||||
their own songs.</p>
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@ -207,7 +202,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Most played songs last 14 days</h2>
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@ -21,10 +21,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Most played songs</h2>
|
<h2>Most played songs</h2>
|
||||||
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different songs a total of
|
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different
|
||||||
<strong> {{ stats.total_requests }} </strong> times. This means
|
songs a total of <strong> {{ stats.total_requests }} </strong> times. This
|
||||||
<strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests
|
means <strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests have been unique. </p>
|
||||||
have been unique. These are the song you have requested the most.</p>
|
|
||||||
<h4>Top {{ stats.stats_top_count }}:</h4>
|
<h4>Top {{ stats.stats_top_count }}:</h4>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@ -51,7 +50,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Most played artists</h2>
|
<h2>Most played artists</h2>
|
||||||
<p>These are the artists you have requested the most.</p>
|
|
||||||
<h4>Top {{ stats.stats_top_count }}:</h4>
|
<h4>Top {{ stats.stats_top_count }}:</h4>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@ -59,7 +57,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Artist</th>
|
<th>Artist</th>
|
||||||
<th style="white-space:nowrap; text-align: right;"># Requests</th>
|
<th style="text-align: right;"># Requests</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -76,11 +74,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Uploads requested</h2>
|
<h2>Uploads requested</h2>
|
||||||
<p>You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
|
<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
|
shows how many times these have been requested by other people. The right column shows
|
||||||
you requested your own songs. In total your songs have been queued
|
how many times you requested your own songs. In total your songs
|
||||||
<strong> {{stats.total_played_uploads }} </strong> times by others and
|
have been queued <strong> {{stats.total_played_uploads }} </strong> times by others and
|
||||||
<strong> {{stats.total_played_user_uploads }} </strong> times by yourself.</p>
|
<strong> {{stats.total_played_user_uploads }} </strong> by yourself.
|
||||||
<h4>Top {{ stats.stats_top_count }}:</h4>
|
<h4>Top {{ stats.stats_top_count }}:</h4>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@ -89,8 +87,8 @@
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Artist</th>
|
<th>Artist</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th style="white-space:nowrap; text-align: right;"># Others</th>
|
<th style="text-align: right;">Others</th>
|
||||||
<th style="white-space:nowrap;"># You</th>
|
<th>You</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -109,8 +107,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Upload artists requested</h2>
|
<h2>Upload artists requested</h2>
|
||||||
<p>The left column shows how many times songs from artists uploaded by you have been requested by
|
<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>
|
other people. The right column shows how many times you requested those songs.
|
||||||
<h4>Top {{ stats.stats_top_count }}:</h4>
|
<h4>Top {{ stats.stats_top_count }}:</h4>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@ -118,8 +116,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Artist</th>
|
<th>Artist</th>
|
||||||
<th style="white-space:nowrap; text-align: right;"># Others</th>
|
<th style="text-align: right;">Others</th>
|
||||||
<th style="white-space:nowrap;"># You</th>
|
<th>You</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -137,15 +135,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Most played uploaders</h2>
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Uploader</th>
|
<th>Uploader</th>
|
||||||
<th style="white-space:nowrap; text-align: right;"># Requests</th>
|
<th style="text-align: right;"># Requests</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -163,14 +160,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Biggest fans</h2>
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th style="white-space:nowrap; text-align: right;"># Requests</th>
|
<th style="text-align: right;"># Requests</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "marietje"
|
name = "MarietjeDjango"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
description = "A music player for the south canteen of the Huygens building"
|
description = "A music player for the south canteen of the Huygens building"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@ -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
|
|
||||||
Reference in New Issue
Block a user