Files
AudiOhm/backend/app/api/v1/playlists.py
T
root a89c7894cf Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube
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>
2026-01-18 20:08:36 +00:00

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",
)