Files
MarietjeDjango/marietje/api/views.py
2019-04-08 18:09:14 +02:00

351 lines
12 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
from marietje.settings import MAX_MINUTES_IN_A_ROW
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, include_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
infobar = {"start_personal_queue": 0, "length_personal_queue": 0, "length_total_queue": 0, "end_personal_queue": 0, 'max_length': MAX_MINUTES_IN_A_ROW}
for song in queue.queue():
infobar["length_total_queue"] += song.song.duration
if song.user == request.user:
infobar["length_personal_queue"] += song.song.duration
infobar["end_personal_queue"] = infobar["length_total_queue"]
if infobar["start_personal_queue"] == 0:
infobar["start_personal_queue"] = infobar["length_total_queue"] - song.song.duration
json = {
'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()),
'user_name': request.user.name,
'infobar': infobar,
}
return JsonResponse(json)
@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_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.delete()
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': err})
request_counter.labels(queue=queue.name).inc()
return JsonResponse({'success': True})
@require_http_methods(["POST"])
@api_auth_required
def report(request):
queue = request.user.queue
song = get_object_or_404(Song, id=request.POST.get('id'), deleted=False)
msg = request.POST.get('msg')
err = song.report(request.user, msg)
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)