Files
MarietjeDjango/marietje/api/views.py
Daan Sprenkels 5c495b17ef Fix timezone and change upload limit to (2*)
I have had enough. People are complaining, I have upper the upload
limit to (2*) instead of (3*). It will be on their heads if the db
is fucked up in a couple of years.
2018-11-08 11:13:50 +01:00

341 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)
if queue.request(song, request.user):
request_counter.labels(queue=queue.name).inc()
return JsonResponse({
'success': True
})
return JsonResponse({
'success': False,
'message': 'You cannot request more than ' + str(settings.MAX_MINUTES_IN_A_ROW) + ' minutes in a row.'
})
@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):
ps = ps.astimezone()
if not ps.played_at:
# Request is from the old times, assume good
return True
if not 0 <= ps.played_at.weekday() <= 4:
return False # Queued in the weekend
if not 7 <= ps.played_at.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)