140 lines
4.5 KiB
Python
140 lines
4.5 KiB
Python
"""
|
||
音乐模块视图 - App端
|
||
"""
|
||
from django.db import transaction
|
||
from django.http import StreamingHttpResponse
|
||
from rest_framework import viewsets
|
||
from rest_framework.decorators import action
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from drf_spectacular.utils import extend_schema
|
||
|
||
from utils.response import success, error
|
||
from utils.exceptions import ErrorCode
|
||
from apps.admins.authentication import AppJWTAuthentication
|
||
from apps.users.models import PointsRecord
|
||
from .models import Track
|
||
from .serializers import TrackSerializer, GenerateMusicSerializer
|
||
from .utils import ensure_default_tracks
|
||
|
||
GENERATE_COST = 100
|
||
|
||
|
||
@extend_schema(tags=['音乐'])
|
||
class MusicViewSet(viewsets.ViewSet):
|
||
"""音乐视图集(App端)"""
|
||
|
||
authentication_classes = [AppJWTAuthentication]
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['get'], url_path='playlist')
|
||
def playlist(self, request):
|
||
"""
|
||
获取播放列表
|
||
GET /api/v1/music/playlist/
|
||
自动初始化默认曲目,返回用户歌曲(时间倒序)+ 默认歌曲(末尾)
|
||
"""
|
||
ensure_default_tracks(request.user)
|
||
|
||
user_tracks = Track.objects.filter(
|
||
user=request.user, is_default=False
|
||
).order_by('-created_at')
|
||
default_tracks = Track.objects.filter(
|
||
user=request.user, is_default=True
|
||
).order_by('created_at')
|
||
|
||
playlist_data = (
|
||
TrackSerializer(user_tracks, many=True).data
|
||
+ TrackSerializer(default_tracks, many=True).data
|
||
)
|
||
return success(data={'playlist': playlist_data})
|
||
|
||
def destroy(self, request, pk=None):
|
||
"""
|
||
删除音乐
|
||
DELETE /api/v1/music/{id}/
|
||
"""
|
||
try:
|
||
track = Track.objects.get(id=pk, user=request.user)
|
||
except Track.DoesNotExist:
|
||
return error(code=ErrorCode.TRACK_NOT_FOUND, message='曲目不存在')
|
||
|
||
if track.is_default:
|
||
return error(
|
||
code=ErrorCode.MUSIC_DEFAULT_UNDELETABLE,
|
||
message='默认曲目不可删除'
|
||
)
|
||
|
||
track.delete()
|
||
return success(message='删除成功')
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def favorite(self, request, pk=None):
|
||
"""
|
||
收藏/取消收藏
|
||
POST /api/v1/music/{id}/favorite/
|
||
"""
|
||
try:
|
||
track = Track.objects.get(id=pk, user=request.user)
|
||
except Track.DoesNotExist:
|
||
return error(code=ErrorCode.TRACK_NOT_FOUND, message='曲目不存在')
|
||
|
||
track.is_favorite = not track.is_favorite
|
||
track.save(update_fields=['is_favorite'])
|
||
|
||
return success(
|
||
data={'is_favorite': track.is_favorite},
|
||
message='已收藏' if track.is_favorite else '已取消收藏'
|
||
)
|
||
|
||
@action(detail=False, methods=['post'], url_path='generate')
|
||
def generate(self, request):
|
||
"""
|
||
生成音乐 (SSE 流式)
|
||
POST /api/v1/music/generate/
|
||
消耗 100 积分,先扣后退(失败退还)
|
||
"""
|
||
serializer = GenerateMusicSerializer(data=request.data)
|
||
if not serializer.is_valid():
|
||
return error(message=str(serializer.errors))
|
||
|
||
text = serializer.validated_data.get('text', '')
|
||
mood = serializer.validated_data.get('mood', 'custom')
|
||
user = request.user
|
||
|
||
# 积分校验
|
||
if user.points < GENERATE_COST:
|
||
return error(
|
||
code=ErrorCode.POINTS_NOT_ENOUGH,
|
||
message=f'积分不足,需要 {GENERATE_COST} 积分,当前 {user.points} 积分'
|
||
)
|
||
|
||
# 原子操作:扣积分 + 创建 Track 占位
|
||
with transaction.atomic():
|
||
user.points -= GENERATE_COST
|
||
user.save(update_fields=['points'])
|
||
|
||
PointsRecord.objects.create(
|
||
user=user,
|
||
amount=-GENERATE_COST,
|
||
type='generate_music',
|
||
description='生成音乐',
|
||
)
|
||
|
||
track = Track.objects.create(
|
||
user=user,
|
||
title='生成中...',
|
||
mood=mood,
|
||
prompt=text,
|
||
generation_status='generating',
|
||
)
|
||
|
||
from .services.music_generation_service import generate_music_stream
|
||
|
||
response = StreamingHttpResponse(
|
||
generate_music_stream(user, track, text, mood),
|
||
content_type='text/event-stream',
|
||
)
|
||
response['Cache-Control'] = 'no-cache'
|
||
response['X-Accel-Buffering'] = 'no'
|
||
return response
|