a89c7894cf
Backend: - FastAPI avec PostgreSQL et Redis - Authentification JWT complète - API REST pour musique, playlists, recherche - Streaming audio via yt-dlp - SQLAlchemy 2.0 async Frontend: - Flutter avec thème néon cyberpunk - State management Riverpod - Layout adaptatif desktop/mobile - Lecteur audio avec mini-player Infrastructure: - Docker Compose (PostgreSQL + Redis) - Scripts d'installation automatisés - Scripts de build pour exécutables Fichiers ajoutés: - BUILD_CLIENT_*.bat/sh: Scripts de compilation - BUILD_CLIENT_README.md: Documentation compilation - CHECK_FLUTTER.sh: Vérificateur d'environnement - requirements.txt mis à jour pour Python 3.13 - Modèles SQLAlchemy corrigés (metadata -> extra_metadata) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
351 lines
10 KiB
Python
351 lines
10 KiB
Python
"""Playlists API routes."""
|
|
from typing import List
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, status
|
|
|
|
from app.api.dependencies import CurrentUser, DBSession
|
|
from app.schemas.playlist import (
|
|
AddTrackRequest,
|
|
PlaylistCreate,
|
|
PlaylistResponse,
|
|
PlaylistWithTracks,
|
|
PlaylistUpdate,
|
|
PlaylistTrackResponse,
|
|
ReorderTracksRequest,
|
|
)
|
|
from app.services.playlist_service import PlaylistService
|
|
|
|
router = APIRouter(prefix="/playlists", tags=["playlists"])
|
|
|
|
|
|
@router.get("", response_model=List[PlaylistResponse])
|
|
async def get_playlists(
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
limit: int = Query(50, ge=1, le=100),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
"""
|
|
Get all playlists for current user.
|
|
|
|
- **limit**: Maximum results (1-100)
|
|
- **offset**: Pagination offset
|
|
"""
|
|
playlist_service = PlaylistService(db)
|
|
playlists = await playlist_service.get_user_playlists(
|
|
user_id=current_user.id,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return [PlaylistResponse.model_validate(p) for p in playlists]
|
|
|
|
|
|
@router.post("", response_model=PlaylistResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_playlist(
|
|
playlist_data: PlaylistCreate,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
):
|
|
"""
|
|
Create a new playlist.
|
|
|
|
- **name**: Playlist name (required)
|
|
- **description**: Optional description
|
|
- **image_url**: Optional cover image URL
|
|
- **is_public**: Whether playlist is public (default: false)
|
|
- **is_collaborative**: Whether playlist is collaborative (default: false)
|
|
"""
|
|
playlist_service = PlaylistService(db)
|
|
playlist = await playlist_service.create_playlist(
|
|
user_id=current_user.id,
|
|
name=playlist_data.name,
|
|
description=playlist_data.description,
|
|
image_url=playlist_data.image_url,
|
|
is_public=playlist_data.is_public,
|
|
is_collaborative=playlist_data.is_collaborative,
|
|
)
|
|
return PlaylistResponse.model_validate(playlist)
|
|
|
|
|
|
@router.get("/{playlist_id}", response_model=PlaylistWithTracks)
|
|
async def get_playlist(
|
|
playlist_id: str,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
include_tracks: bool = Query(True, description="Include tracks in response"),
|
|
):
|
|
"""
|
|
Get a playlist by ID.
|
|
|
|
- **include_tracks**: Whether to include tracks (default: true)
|
|
"""
|
|
from uuid import UUID
|
|
|
|
playlist_service = PlaylistService(db)
|
|
|
|
try:
|
|
playlist = await playlist_service.get_playlist(
|
|
UUID(playlist_id),
|
|
include_tracks=include_tracks,
|
|
)
|
|
|
|
if not playlist:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Playlist not found",
|
|
)
|
|
|
|
# Check access permissions
|
|
if not playlist.is_public and playlist.user_id != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Not authorized to view this playlist",
|
|
)
|
|
|
|
response = PlaylistWithTracks.model_validate(playlist)
|
|
|
|
if include_tracks and playlist.playlist_tracks:
|
|
# Load tracks with details
|
|
from sqlalchemy import select
|
|
from app.models.track import Track
|
|
|
|
track_ids = [pt.track_id for pt in playlist.playlist_tracks]
|
|
stmt = (
|
|
select(Track)
|
|
.where(Track.id.in_(track_ids))
|
|
)
|
|
result = await db.execute(stmt)
|
|
tracks = {t.id: t for t in result.scalars().all()}
|
|
|
|
# Build response with track details
|
|
response.tracks = [
|
|
{
|
|
"id": str(pt.track_id),
|
|
"position": pt.position,
|
|
"added_at": pt.added_at.isoformat(),
|
|
"added_by": str(pt.added_by) if pt.added_by else None,
|
|
"track": {
|
|
"id": str(tracks[pt.track_id].id),
|
|
"title": tracks[pt.track_id].title,
|
|
"duration": tracks[pt.track_id].duration,
|
|
"artist": tracks[pt.track_id].artist.name if tracks[pt.track_id].artist else None,
|
|
"image_url": tracks[pt.track_id].image_url,
|
|
} if pt.track_id in tracks else None,
|
|
}
|
|
for pt in sorted(playlist.playlist_tracks, key=lambda x: x.position)
|
|
]
|
|
|
|
return response
|
|
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid playlist ID",
|
|
)
|
|
|
|
|
|
@router.put("/{playlist_id}", response_model=PlaylistResponse)
|
|
async def update_playlist(
|
|
playlist_id: str,
|
|
playlist_data: PlaylistUpdate,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
):
|
|
"""
|
|
Update a playlist.
|
|
|
|
Only the owner can update a playlist.
|
|
"""
|
|
from uuid import UUID
|
|
|
|
playlist_service = PlaylistService(db)
|
|
|
|
try:
|
|
playlist = await playlist_service.update_playlist(
|
|
playlist_id=UUID(playlist_id),
|
|
user_id=current_user.id,
|
|
name=playlist_data.name,
|
|
description=playlist_data.description,
|
|
image_url=playlist_data.image_url,
|
|
is_public=playlist_data.is_public,
|
|
)
|
|
return PlaylistResponse.model_validate(playlist)
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=str(e),
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid playlist ID",
|
|
)
|
|
|
|
|
|
@router.delete("/{playlist_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_playlist(
|
|
playlist_id: str,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
):
|
|
"""
|
|
Delete a playlist.
|
|
|
|
Only the owner can delete a playlist.
|
|
"""
|
|
from uuid import UUID
|
|
|
|
playlist_service = PlaylistService(db)
|
|
|
|
try:
|
|
await playlist_service.delete_playlist(
|
|
playlist_id=UUID(playlist_id),
|
|
user_id=current_user.id,
|
|
)
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=str(e),
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid playlist ID",
|
|
)
|
|
|
|
|
|
@router.post("/{playlist_id}/tracks", response_model=PlaylistResponse)
|
|
async def add_tracks(
|
|
playlist_id: str,
|
|
track_data: AddTrackRequest,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
):
|
|
"""
|
|
Add tracks to a playlist.
|
|
|
|
- **track_ids**: List of track UUIDs to add (1-100 tracks)
|
|
- **position**: Optional starting position (default: end of playlist)
|
|
"""
|
|
from uuid import UUID
|
|
|
|
playlist_service = PlaylistService(db)
|
|
|
|
try:
|
|
playlist = await playlist_service.add_tracks(
|
|
playlist_id=UUID(playlist_id),
|
|
track_ids=track_data.track_ids,
|
|
user_id=current_user.id,
|
|
position=track_data.position,
|
|
)
|
|
return PlaylistResponse.model_validate(playlist)
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(e),
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid playlist or track ID",
|
|
)
|
|
|
|
|
|
@router.delete("/{playlist_id}/tracks/{track_id}", response_model=PlaylistResponse)
|
|
async def remove_track(
|
|
playlist_id: str,
|
|
track_id: str,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
):
|
|
"""
|
|
Remove a track from a playlist.
|
|
|
|
Only the owner can remove tracks.
|
|
"""
|
|
from uuid import UUID
|
|
|
|
playlist_service = PlaylistService(db)
|
|
|
|
try:
|
|
playlist = await playlist_service.remove_track(
|
|
playlist_id=UUID(playlist_id),
|
|
track_id=UUID(track_id),
|
|
user_id=current_user.id,
|
|
)
|
|
return PlaylistResponse.model_validate(playlist)
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=str(e),
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid playlist or track ID",
|
|
)
|
|
|
|
|
|
@router.put("/{playlist_id}/tracks/reorder", response_model=PlaylistResponse)
|
|
async def reorder_track(
|
|
playlist_id: str,
|
|
reorder_data: ReorderTracksRequest,
|
|
current_user: CurrentUser,
|
|
db: DBSession,
|
|
):
|
|
"""
|
|
Reorder a track within a playlist.
|
|
|
|
- **track_id**: Track UUID to reorder
|
|
- **new_position**: New position (0-indexed)
|
|
|
|
Only the owner can reorder tracks.
|
|
"""
|
|
from uuid import UUID
|
|
|
|
playlist_service = PlaylistService(db)
|
|
|
|
try:
|
|
playlist = await playlist_service.reorder_track(
|
|
playlist_id=UUID(playlist_id),
|
|
track_id=reorder_data.track_id,
|
|
new_position=reorder_data.new_position,
|
|
user_id=current_user.id,
|
|
)
|
|
return PlaylistResponse.model_validate(playlist)
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=str(e),
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid playlist or track ID",
|
|
)
|