mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2025-12-09 23:12:21 +01:00
337 lines
11 KiB
Python
337 lines
11 KiB
Python
import time
|
|
from functools import wraps
|
|
|
|
import django.middleware.csrf as csrf
|
|
from django.contrib.auth import authenticate, login
|
|
from django.core.cache import caches
|
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|
from django.db import transaction
|
|
from django.db.models import Q, Sum, Value
|
|
from django.db.models.functions import Coalesce
|
|
from django.http import JsonResponse, HttpResponseForbidden
|
|
from django.shortcuts import get_object_or_404
|
|
from django.views.decorators.cache import cache_page
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.conf import settings
|
|
from mutagen import File
|
|
from prometheus_client import Counter
|
|
|
|
|
|
from marietje.utils import song_to_dict, playlist_song_to_dict, send_to_bertha
|
|
from queues.models import PlaylistSong, QueueCommand
|
|
from songs.models import Song
|
|
|
|
request_counter = Counter('marietje_requests', 'Queue requests on marietje', ['queue'])
|
|
upload_counter = Counter('marietje_uploads', 'Songs uploaded to marietje')
|
|
|
|
|
|
def api_auth_required(view_func):
|
|
@wraps(view_func)
|
|
def _wrapped_view(request, *args, **kwargs):
|
|
if request.user.is_authenticated and request.user.is_active:
|
|
return view_func(request, *args, **kwargs)
|
|
response = JsonResponse({
|
|
'error': 'User not authenticated or activated.'
|
|
})
|
|
response.status_code = 401
|
|
return response
|
|
|
|
return _wrapped_view
|
|
|
|
|
|
def login_user(request):
|
|
data = {'error': 'Method not allowed'}
|
|
status = 405
|
|
if request.method == "POST":
|
|
username = request.POST.get('username', '').strip()
|
|
password = request.POST.get('password', '').strip()
|
|
|
|
data = {'error': 'Please enter a correct username and password. '
|
|
'Note that both fields may be case-sensitive.'}
|
|
status = 401
|
|
|
|
if username and password:
|
|
user = authenticate(username=username, password=password)
|
|
if user is not None:
|
|
if user.is_active:
|
|
login(request, user)
|
|
data = {}
|
|
status = 200
|
|
else:
|
|
data = {'error': 'User is not active'}
|
|
status = 401
|
|
else:
|
|
csrf.get_token(request)
|
|
response = JsonResponse(data)
|
|
response.status_code = status
|
|
return response
|
|
|
|
|
|
@api_auth_required
|
|
def permissions(request):
|
|
return JsonResponse({
|
|
'can_move': request.user.has_perm('queues.can_move'),
|
|
'can_skip': request.user.has_perm('queues.can_skip'),
|
|
'can_cancel': request.user.has_perm('queues.can_cancel'),
|
|
'can_control_volume': request.user.has_perm('queues.can_control_volume')
|
|
})
|
|
|
|
|
|
@api_auth_required
|
|
def songs(request):
|
|
try:
|
|
pagesize = int(request.POST.get('pagesize'))
|
|
except:
|
|
pagesize = 50
|
|
|
|
if not pagesize or pagesize < 50:
|
|
pagesize = 50
|
|
|
|
try:
|
|
page = int(request.POST.get('page'))
|
|
except:
|
|
page = 1
|
|
|
|
def search_songs():
|
|
queries = [Q(deleted=False)]
|
|
queries.extend([Q(Q(artist__icontains=word) | Q(title__icontains=word)) for word in request.POST.get('all', '').split()])
|
|
queries.extend([Q(user__name__icontains=word) for word in request.POST.get('uploader', '').split()])
|
|
|
|
filter_query = queries.pop()
|
|
for query in queries:
|
|
filter_query &= query
|
|
|
|
songs_query = Song.objects.filter(filter_query).order_by('artist', 'title').select_related('user')
|
|
paginator = Paginator(songs_query, pagesize)
|
|
|
|
try:
|
|
songs = paginator.page(page)
|
|
except PageNotAnInteger:
|
|
songs = paginator.page(1)
|
|
except EmptyPage:
|
|
songs = paginator.page(paginator.num_pages)
|
|
|
|
songs_dict = [song_to_dict(song, user=True) for song in songs.object_list]
|
|
return JsonResponse({
|
|
'per_page': pagesize,
|
|
'current_page': page,
|
|
'last_page': paginator.num_pages,
|
|
'data': songs_dict,
|
|
})
|
|
|
|
cache_key = '|'.join(request.POST.get(k, '') for k in ('all', 'uploader', 'pagesize', 'page'))
|
|
return caches['song_search'].get_or_set(cache_key, search_songs, 60*60*2)
|
|
|
|
|
|
@api_auth_required
|
|
def managesongs(request):
|
|
try:
|
|
pagesize = int(request.POST.get('pagesize'))
|
|
except:
|
|
pagesize = 10
|
|
|
|
if not pagesize or pagesize < 10:
|
|
pagesize = 10
|
|
|
|
try:
|
|
page = int(request.POST.get('page'))
|
|
except:
|
|
page = 1
|
|
|
|
songs_query = Song.objects.filter(
|
|
user=request.user, deleted=False,
|
|
artist__icontains=request.POST.get('artist', ''),
|
|
title__icontains=request.POST.get('title', '')
|
|
).order_by('artist', 'title')
|
|
|
|
total = songs_query.count()
|
|
paginator = Paginator(songs_query, pagesize)
|
|
|
|
try:
|
|
songs = paginator.page(page)
|
|
except PageNotAnInteger:
|
|
songs = paginator.page(1)
|
|
except EmptyPage:
|
|
songs = paginator.page(paginator.num_pages)
|
|
|
|
songs_dict = [song_to_dict(song) for song in songs.object_list]
|
|
|
|
return JsonResponse({
|
|
'total': total,
|
|
'per_page': pagesize,
|
|
'current_page': page,
|
|
'last_page': paginator.num_pages,
|
|
'data': songs_dict
|
|
})
|
|
|
|
|
|
@api_auth_required
|
|
def queue(request):
|
|
queue = request.user.queue
|
|
return JsonResponse({
|
|
'current_song': playlist_song_to_dict(queue.current_song()),
|
|
'queue': [playlist_song_to_dict(playlist_song, user=request.user) for playlist_song in queue.queue()],
|
|
'started_at': 0 if queue.started_at is None else int(queue.started_at.timestamp()),
|
|
'current_time': int(time.time())
|
|
})
|
|
|
|
|
|
@api_auth_required
|
|
def skip(request):
|
|
playlist_song = request.user.queue.current_song()
|
|
if playlist_song.user != request.user and not request.user.has_perm('queues.can_skip'):
|
|
return HttpResponseForbidden()
|
|
|
|
playlist_song.state = 2
|
|
playlist_song.save()
|
|
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def move_up(request):
|
|
if not request.user.has_perm('queues.can_move'):
|
|
return HttpResponseForbidden()
|
|
playlist_song = get_object_or_404(PlaylistSong, id=request.POST.get('id'))
|
|
playlist_song.move_up()
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def move_down(request):
|
|
playlist_song = get_object_or_404(PlaylistSong, id=request.POST.get('id'))
|
|
if playlist_song.user != request.user and not request.user.has_perm('queues.can_move'):
|
|
return HttpResponseForbidden()
|
|
playlist_song.move_down()
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def cancel(request):
|
|
playlist_song = get_object_or_404(PlaylistSong, id=request.POST.get('id'))
|
|
if playlist_song.user != request.user and not request.user.has_perm('queues.can_cancel'):
|
|
return HttpResponseForbidden()
|
|
playlist_song.state = 3
|
|
playlist_song.save()
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def request(request):
|
|
queue = request.user.queue
|
|
song = get_object_or_404(Song, id=request.POST.get('id'), deleted=False)
|
|
|
|
err = queue.request(song, request.user)
|
|
if err != None:
|
|
return JsonResponse({ 'success': False, 'message': msg })
|
|
|
|
request_counter.labels(queue=queue.name).inc()
|
|
return JsonResponse({ 'success': True })
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def upload(request):
|
|
files = request.FILES.getlist('file[]')
|
|
artists = request.POST.getlist('artist[]')
|
|
titles = request.POST.getlist('title[]')
|
|
|
|
for artist in artists:
|
|
if not artist:
|
|
return JsonResponse({'success': False, 'errorMessage': 'Please enter artists which are not empty.'})
|
|
|
|
for title in titles:
|
|
if not title:
|
|
return JsonResponse({'success': False, 'errorMessage': 'Please enter titles which are not empty.'})
|
|
|
|
# Allow upload if the user has a good reputation
|
|
# Score function:
|
|
# - U = duration * songs uploaded
|
|
# - Q = duration * songs queued
|
|
# - If 2*U < Q: allow upload (otherwise don't)
|
|
try:
|
|
stats = upload_stats(request.user)
|
|
ratio = stats['queued_score'] / (2.0 * stats['upload_score'])
|
|
ratiostr = '{:.2f}'.format(ratio)
|
|
except ZeroDivisionError:
|
|
ratio = 99999.0 # high enough
|
|
ratiostr = "∞"
|
|
|
|
if not request.user.is_superuser and ratio < 1.0:
|
|
msg = 'Queue-to-upload ratio too low. Please queue more during regular opening hours to improve the ratio. (Ratio: {} ≱ 1.00)'
|
|
return JsonResponse({'success': False, 'errorMessage': msg.format(ratiostr)})
|
|
|
|
for i, file in enumerate(files):
|
|
duration = File(file).info.length
|
|
hash = send_to_bertha(file).decode('ascii')
|
|
if not hash:
|
|
return JsonResponse({'success': False, 'errorMessage': 'Files not uploaded correctly.'})
|
|
song = Song(user=request.user, artist=artists[i], title=titles[i], hash=hash, duration=duration)
|
|
song.save()
|
|
|
|
# Clear the search cache
|
|
caches['song_search'].clear()
|
|
|
|
upload_counter.inc()
|
|
return JsonResponse({'success': True})
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def volume_down(request):
|
|
if not request.user.has_perm('queues.can_control_volume'):
|
|
return HttpResponseForbidden()
|
|
command = QueueCommand(queue=request.user.queue, command='volume_down')
|
|
command.save()
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def volume_up(request):
|
|
if not request.user.has_perm('queues.can_control_volume'):
|
|
return HttpResponseForbidden()
|
|
command = QueueCommand(queue=request.user.queue, command='volume_up')
|
|
command.save()
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@api_auth_required
|
|
def mute(request):
|
|
if not request.user.has_perm('queues.can_control_volume'):
|
|
return HttpResponseForbidden()
|
|
command = QueueCommand(queue=request.user.queue, command='mute')
|
|
command.save()
|
|
return JsonResponse({})
|
|
|
|
def upload_stats(user):
|
|
songs_queued = PlaylistSong.objects.select_related('song').filter(
|
|
user=user, state=2, song__deleted=False)
|
|
queued_score = sum(_request_weight(x) for x in songs_queued)
|
|
upload_score = Song.objects.filter(
|
|
user=user, deleted=False).aggregate(
|
|
x=Coalesce(Sum('duration'), Value(0)))['x']
|
|
return {'queued_score': queued_score, 'upload_score': upload_score}
|
|
|
|
@transaction.atomic
|
|
def _request_weight(ps):
|
|
def _is_regular_queue(ps):
|
|
if not ps.played_at:
|
|
# Request is from the old times, assume good
|
|
return True
|
|
if not 0 <= ps.played_at.astimezone().weekday() <= 4:
|
|
return False # Queued in the weekend
|
|
if not 7 <= ps.played_at.astimezone().hour <= 22:
|
|
# Because of timezone shit, I allow for an extra hour of leeway
|
|
return False # Queued outside of regular opening hours
|
|
return True
|
|
|
|
if _is_regular_queue(ps):
|
|
return float(ps.song.duration)
|
|
# Count other requests for 10%
|
|
return 0.10 * float(ps.song.duration)
|