import time from functools import wraps import django.middleware.csrf as csrf from django.contrib.auth import authenticate, login 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.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 = 10 if not pagesize or pagesize < 10: pagesize = 10 try: page = int(request.POST.get('page')) except: page = 1 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 }) @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 3*U < Q: allow upload (otherwise don't) try: stats = upload_stats(request.user) ratio = stats['minutes_queued'] / (3.0 * stats['minutes_upload']) 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 high. Please queue some more before uploading. ({})' 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() 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({}) @transaction.atomic def upload_stats(user): q = PlaylistSong.objects.filter(user=user, song__deleted=False).aggregate( minutes_queued=Coalesce(Sum('song__duration'), Value(0))) q.update(Song.objects.filter(user=user, deleted=False).aggregate( minutes_upload=Coalesce(Sum('duration'), Value(0)))) return q