Fix: Include backend/audio Django app in repository

This commit is contained in:
Iulian 2025-12-24 01:58:56 +00:00
parent d04e726373
commit 644cfab298
37 changed files with 6632 additions and 4 deletions

View file

@ -0,0 +1,201 @@
"""Views for lyrics management"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import get_object_or_404
from audio.models import Audio
from audio.models_lyrics import Lyrics, LyricsCache
from audio.serializers_lyrics import (
LyricsSerializer,
LyricsUpdateSerializer,
LyricsFetchSerializer,
LyricsCacheSerializer,
)
from audio.lyrics_service import LyricsService
from audio.tasks_lyrics import fetch_lyrics_for_audio, fetch_lyrics_batch
class LyricsViewSet(viewsets.ModelViewSet):
"""ViewSet for managing lyrics"""
permission_classes = [IsAuthenticated]
serializer_class = LyricsSerializer
lookup_field = 'audio__youtube_id'
lookup_url_kwarg = 'youtube_id'
def get_queryset(self):
"""Get lyrics queryset"""
return Lyrics.objects.select_related('audio').all()
def retrieve(self, request, youtube_id=None):
"""Get lyrics for a specific audio track"""
audio = get_object_or_404(Audio, youtube_id=youtube_id)
# Get or create lyrics entry
lyrics, created = Lyrics.objects.get_or_create(audio=audio)
# If no lyrics and not attempted, trigger fetch
if not lyrics.fetch_attempted:
# Trigger async fetch
fetch_lyrics_for_audio.delay(youtube_id)
serializer = self.get_serializer(lyrics)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def fetch(self, request, youtube_id=None):
"""Manually fetch lyrics for an audio track"""
audio = get_object_or_404(Audio, youtube_id=youtube_id)
# Validate request data
fetch_serializer = LyricsFetchSerializer(data=request.data)
fetch_serializer.is_valid(raise_exception=True)
force = fetch_serializer.validated_data.get('force', False)
# Fetch lyrics synchronously
service = LyricsService()
lyrics = service.fetch_and_store_lyrics(audio, force=force)
serializer = self.get_serializer(lyrics)
return Response(serializer.data)
@action(detail=True, methods=['put', 'patch'])
def update_lyrics(self, request, youtube_id=None):
"""Manually update lyrics for an audio track"""
audio = get_object_or_404(Audio, youtube_id=youtube_id)
lyrics, created = Lyrics.objects.get_or_create(audio=audio)
# Validate and update
update_serializer = LyricsUpdateSerializer(data=request.data)
update_serializer.is_valid(raise_exception=True)
# Update fields
if 'synced_lyrics' in update_serializer.validated_data:
lyrics.synced_lyrics = update_serializer.validated_data['synced_lyrics']
if 'plain_lyrics' in update_serializer.validated_data:
lyrics.plain_lyrics = update_serializer.validated_data['plain_lyrics']
if 'is_instrumental' in update_serializer.validated_data:
lyrics.is_instrumental = update_serializer.validated_data['is_instrumental']
if 'language' in update_serializer.validated_data:
lyrics.language = update_serializer.validated_data['language']
lyrics.source = 'manual'
lyrics.fetch_attempted = True
lyrics.save()
serializer = self.get_serializer(lyrics)
return Response(serializer.data)
@action(detail=True, methods=['delete'])
def delete_lyrics(self, request, youtube_id=None):
"""Delete lyrics for an audio track"""
audio = get_object_or_404(Audio, youtube_id=youtube_id)
try:
lyrics = Lyrics.objects.get(audio=audio)
lyrics.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Lyrics.DoesNotExist:
return Response(
{'message': 'No lyrics found for this track'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=False, methods=['post'])
def fetch_batch(self, request):
"""Fetch lyrics for multiple audio tracks"""
youtube_ids = request.data.get('youtube_ids', [])
if not youtube_ids:
return Response(
{'error': 'youtube_ids is required'},
status=status.HTTP_400_BAD_REQUEST
)
# Trigger async batch fetch
fetch_lyrics_batch.delay(youtube_ids)
return Response({
'message': f'Fetching lyrics for {len(youtube_ids)} tracks',
'youtube_ids': youtube_ids,
})
@action(detail=False, methods=['post'])
def fetch_all_missing(self, request):
"""Fetch lyrics for all audio without lyrics"""
from audio.tasks_lyrics import auto_fetch_lyrics
limit = request.data.get('limit', 50)
# Trigger async task
result = auto_fetch_lyrics.delay(limit=limit)
return Response({
'message': f'Fetching lyrics for up to {limit} tracks without lyrics',
'task_id': result.id,
})
@action(detail=False, methods=['get'])
def stats(self, request):
"""Get lyrics statistics"""
total_audio = Audio.objects.filter(downloaded=True).count()
total_lyrics = Lyrics.objects.filter(fetch_attempted=True).count()
with_synced = Lyrics.objects.exclude(synced_lyrics='').count()
with_plain = Lyrics.objects.exclude(plain_lyrics='').count()
instrumental = Lyrics.objects.filter(is_instrumental=True).count()
failed = Lyrics.objects.filter(
fetch_attempted=True,
synced_lyrics='',
plain_lyrics='',
is_instrumental=False
).count()
return Response({
'total_audio': total_audio,
'total_lyrics_attempted': total_lyrics,
'with_synced_lyrics': with_synced,
'with_plain_lyrics': with_plain,
'instrumental': instrumental,
'failed': failed,
'coverage_percentage': round((with_synced + with_plain + instrumental) / total_audio * 100, 1) if total_audio > 0 else 0,
})
class LyricsCacheViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for viewing lyrics cache"""
permission_classes = [IsAuthenticated]
serializer_class = LyricsCacheSerializer
queryset = LyricsCache.objects.all()
@action(detail=False, methods=['post'])
def cleanup(self, request):
"""Clean up old cache entries"""
from audio.tasks_lyrics import cleanup_lyrics_cache
days_old = request.data.get('days_old', 30)
result = cleanup_lyrics_cache.delay(days_old=days_old)
return Response({
'message': f'Cleaning up cache entries older than {days_old} days',
'task_id': result.id,
})
@action(detail=False, methods=['get'])
def stats(self, request):
"""Get cache statistics"""
total = LyricsCache.objects.count()
not_found = LyricsCache.objects.filter(not_found=True).count()
with_synced = LyricsCache.objects.exclude(synced_lyrics='').count()
with_plain = LyricsCache.objects.exclude(plain_lyrics='').count()
return Response({
'total_entries': total,
'not_found_entries': not_found,
'with_synced_lyrics': with_synced,
'with_plain_lyrics': with_plain,
'hit_rate': round((with_synced + with_plain) / total * 100, 1) if total > 0 else 0,
})