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>
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
"""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",
|
||||
)
|
||||
Reference in New Issue
Block a user