Fix: Include backend/audio Django app in repository
This commit is contained in:
parent
d04e726373
commit
644cfab298
37 changed files with 6632 additions and 4 deletions
266
backend/audio/views.py
Normal file
266
backend/audio/views.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
"""Audio API views"""
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from audio.models import Audio, AudioProgress
|
||||
from audio.serializers import (
|
||||
AudioListSerializer,
|
||||
AudioSerializer,
|
||||
AudioProgressUpdateSerializer,
|
||||
PlayerSerializer,
|
||||
)
|
||||
from common.views import ApiBaseView, AdminWriteOnly
|
||||
|
||||
|
||||
class AudioListView(ApiBaseView):
|
||||
"""Audio list endpoint
|
||||
GET: returns list of audio files
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
"""Get audio list"""
|
||||
# Get query parameters
|
||||
channel_id = request.query_params.get('channel')
|
||||
playlist_id = request.query_params.get('playlist')
|
||||
status_filter = request.query_params.get('status')
|
||||
|
||||
# Base queryset - filter by user
|
||||
queryset = Audio.objects.filter(owner=request.user)
|
||||
|
||||
# Apply filters
|
||||
if channel_id:
|
||||
queryset = queryset.filter(channel_id=channel_id)
|
||||
if playlist_id:
|
||||
# TODO: Filter by playlist
|
||||
pass
|
||||
if status_filter:
|
||||
# TODO: Filter by play status
|
||||
pass
|
||||
|
||||
# Pagination
|
||||
page_size = 50
|
||||
page = int(request.query_params.get('page', 1))
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
|
||||
audio_list = queryset[start:end]
|
||||
serializer = AudioSerializer(audio_list, many=True)
|
||||
|
||||
return Response({
|
||||
'data': serializer.data,
|
||||
'paginate': True
|
||||
})
|
||||
|
||||
|
||||
class AudioDetailView(ApiBaseView):
|
||||
"""Audio detail endpoint
|
||||
GET: returns single audio file details
|
||||
POST: trigger actions (download)
|
||||
DELETE: delete audio file
|
||||
"""
|
||||
permission_classes = [AdminWriteOnly]
|
||||
|
||||
def get(self, request, youtube_id):
|
||||
"""Get audio details"""
|
||||
audio = get_object_or_404(Audio, youtube_id=youtube_id, owner=request.user)
|
||||
serializer = AudioSerializer(audio)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, youtube_id):
|
||||
"""Trigger actions on audio"""
|
||||
audio = get_object_or_404(Audio, youtube_id=youtube_id, owner=request.user)
|
||||
action = request.data.get('action')
|
||||
|
||||
if action == 'download':
|
||||
# Check if already downloaded
|
||||
if audio.file_path:
|
||||
return Response(
|
||||
{'detail': 'Audio already downloaded', 'status': 'already_downloaded'},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# Add to download queue
|
||||
from download.models import DownloadQueue
|
||||
from task.tasks import download_audio_task
|
||||
|
||||
# Create download queue item
|
||||
queue_item, created = DownloadQueue.objects.get_or_create(
|
||||
owner=request.user,
|
||||
youtube_id=youtube_id,
|
||||
defaults={
|
||||
'url': f'https://www.youtube.com/watch?v={youtube_id}',
|
||||
'title': audio.title,
|
||||
'channel_name': audio.channel_name,
|
||||
'auto_start': True,
|
||||
}
|
||||
)
|
||||
|
||||
# Trigger download task
|
||||
if created or queue_item.status == 'failed':
|
||||
download_audio_task.delay(queue_item.id)
|
||||
return Response(
|
||||
{'detail': 'Download started', 'status': 'downloading'},
|
||||
status=status.HTTP_202_ACCEPTED
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{'detail': 'Download already in progress', 'status': queue_item.status},
|
||||
status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(
|
||||
{'detail': 'Invalid action'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def delete(self, request, youtube_id):
|
||||
"""Delete audio file"""
|
||||
audio = get_object_or_404(Audio, youtube_id=youtube_id, owner=request.user)
|
||||
audio.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class AudioPlayerView(ApiBaseView):
|
||||
"""Audio player endpoint
|
||||
GET: returns audio player data with stream URL
|
||||
"""
|
||||
|
||||
def get(self, request, youtube_id):
|
||||
"""Get player data"""
|
||||
audio = get_object_or_404(Audio, youtube_id=youtube_id, owner=request.user)
|
||||
|
||||
# Trigger lyrics fetch if not already fetched (async, non-blocking)
|
||||
try:
|
||||
if not hasattr(audio, 'lyrics') or not audio.lyrics.fetch_attempted:
|
||||
from audio.tasks_lyrics import fetch_lyrics_for_audio
|
||||
fetch_lyrics_for_audio.delay(youtube_id)
|
||||
except Exception:
|
||||
pass # Don't block playback if lyrics fetch fails
|
||||
|
||||
# Get user progress
|
||||
progress = None
|
||||
try:
|
||||
progress = AudioProgress.objects.get(user=request.user, audio=audio)
|
||||
except AudioProgress.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Build stream URL with proper encoding for special characters
|
||||
from urllib.parse import quote
|
||||
# Encode the file path, preserving forward slashes
|
||||
encoded_path = '/'.join(quote(part, safe='') for part in audio.file_path.split('/'))
|
||||
stream_url = f"/media/{encoded_path}"
|
||||
|
||||
data = {
|
||||
'audio': AudioSerializer(audio).data,
|
||||
'stream_url': stream_url
|
||||
}
|
||||
if progress:
|
||||
data['progress'] = {
|
||||
'position': progress.position,
|
||||
'completed': progress.completed
|
||||
}
|
||||
|
||||
return Response(data)
|
||||
|
||||
|
||||
class AudioProgressView(ApiBaseView):
|
||||
"""Audio progress endpoint
|
||||
POST: update playback progress
|
||||
"""
|
||||
|
||||
def post(self, request, youtube_id):
|
||||
"""Update audio progress"""
|
||||
audio = get_object_or_404(Audio, youtube_id=youtube_id, owner=request.user)
|
||||
|
||||
serializer = AudioProgressUpdateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
progress, created = AudioProgress.objects.get_or_create(
|
||||
user=request.user,
|
||||
audio=audio,
|
||||
defaults={
|
||||
'position': serializer.validated_data['position'],
|
||||
'completed': serializer.validated_data.get('completed', False)
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
progress.position = serializer.validated_data['position']
|
||||
progress.completed = serializer.validated_data.get('completed', False)
|
||||
progress.save()
|
||||
|
||||
# Update audio play count
|
||||
if created or serializer.validated_data.get('completed'):
|
||||
audio.play_count += 1
|
||||
audio.save()
|
||||
|
||||
return Response({
|
||||
'position': progress.position,
|
||||
'completed': progress.completed
|
||||
})
|
||||
|
||||
|
||||
class AudioDownloadView(ApiBaseView):
|
||||
"""Audio file download endpoint
|
||||
GET: download audio file to user's device
|
||||
"""
|
||||
|
||||
def get(self, request, youtube_id):
|
||||
"""Download audio file with security checks"""
|
||||
from django.http import FileResponse, Http404
|
||||
import os
|
||||
from django.conf import settings
|
||||
from pathlib import Path
|
||||
|
||||
# Security: Verify ownership
|
||||
audio = get_object_or_404(Audio, youtube_id=youtube_id, owner=request.user)
|
||||
|
||||
if not audio.file_path:
|
||||
raise Http404("Audio file not available")
|
||||
|
||||
# Security: Prevent path traversal attacks
|
||||
file_path = audio.file_path
|
||||
if '..' in file_path or file_path.startswith('/') or '\\' in file_path:
|
||||
raise Http404("Invalid file path")
|
||||
|
||||
# Build and resolve full path
|
||||
full_path = Path(settings.MEDIA_ROOT) / file_path
|
||||
|
||||
# Security: Verify the resolved path is within MEDIA_ROOT
|
||||
try:
|
||||
full_path = full_path.resolve()
|
||||
media_root = Path(settings.MEDIA_ROOT).resolve()
|
||||
full_path.relative_to(media_root)
|
||||
except (ValueError, OSError):
|
||||
raise Http404("Access denied")
|
||||
|
||||
# Verify file exists and is a file (not directory)
|
||||
if not full_path.exists() or not full_path.is_file():
|
||||
raise Http404("Audio file not found on disk")
|
||||
|
||||
# Get file extension and determine content type
|
||||
_, ext = os.path.splitext(str(full_path))
|
||||
content_type = 'audio/mpeg' # Default
|
||||
if ext.lower() in ['.m4a', '.mp4']:
|
||||
content_type = 'audio/mp4'
|
||||
elif ext.lower() == '.opus':
|
||||
content_type = 'audio/opus'
|
||||
elif ext.lower() == '.webm':
|
||||
content_type = 'audio/webm'
|
||||
|
||||
# Create safe filename for download
|
||||
safe_title = "".join(c for c in audio.title if c.isalnum() or c in (' ', '-', '_')).strip()
|
||||
if not safe_title:
|
||||
safe_title = f"audio_{youtube_id}"
|
||||
filename = f"{safe_title}{ext}"
|
||||
|
||||
# Serve file with proper headers
|
||||
response = FileResponse(
|
||||
open(full_path, 'rb'),
|
||||
content_type=content_type,
|
||||
as_attachment=True,
|
||||
filename=filename
|
||||
)
|
||||
|
||||
return response
|
||||
Loading…
Add table
Add a link
Reference in a new issue