diff --git a/app/api/v1/api.py b/app/api/v1/api.py index 0a6b88b..2c2d0e7 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -1,7 +1,7 @@ from fastapi import APIRouter # Import individual routers here -from app.api.v1.endpoints import users, auth, songs, albums, artists, playlists +from app.api.v1.endpoints import users, auth, songs, albums, artists, playlists, streaming, upload api_router = APIRouter() @@ -11,4 +11,6 @@ api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(songs.router, prefix="/songs", tags=["songs"]) api_router.include_router(albums.router, prefix="/albums", tags=["albums"]) api_router.include_router(artists.router, prefix="/artists", tags=["artists"]) -api_router.include_router(playlists.router, prefix="/playlists", tags=["playlists"]) \ No newline at end of file +api_router.include_router(playlists.router, prefix="/playlists", tags=["playlists"]) +api_router.include_router(streaming.router, prefix="/stream", tags=["streaming"]) +api_router.include_router(upload.router, prefix="/upload", tags=["upload"]) \ No newline at end of file diff --git a/app/api/v1/endpoints/streaming.py b/app/api/v1/endpoints/streaming.py new file mode 100644 index 0000000..5dec1e4 --- /dev/null +++ b/app/api/v1/endpoints/streaming.py @@ -0,0 +1,134 @@ +from typing import Any +from pathlib import Path + +from fastapi import APIRouter, Depends, HTTPException, status, Response +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session + +from app.api.deps import get_db, get_current_active_user +from app.models.user import User +from app.services.song import get_song + +router = APIRouter() + + +@router.get("/song/{song_id}", response_class=Response) +async def stream_song( + song_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user), +) -> Any: + """ + Stream a song by ID. Requires authentication. + """ + song = get_song(db, song_id=song_id) + if not song: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Song not found", + ) + + # Construct file path + file_path = Path(song.file_path) + + # Check if file exists + if not file_path.exists(): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Audio file not found", + ) + + # Return file response for streaming + return FileResponse( + path=file_path, + filename=f"{song.title}.mp3", + media_type="audio/mpeg" + ) + + +@router.get("/album/cover/{album_id}") +async def get_album_cover( + album_id: int, + db: Session = Depends(get_db), +) -> Any: + """ + Get album cover image by album ID. + """ + from app.services.album import get_album + + album = get_album(db, album_id=album_id) + if not album or not album.cover_image_path: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Album cover not found", + ) + + # Construct file path + file_path = Path(album.cover_image_path) + + # Check if file exists + if not file_path.exists(): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Cover image file not found", + ) + + # Determine media type based on file extension + extension = file_path.suffix.lower() + media_type = "image/jpeg" # Default + if extension == ".png": + media_type = "image/png" + elif extension == ".gif": + media_type = "image/gif" + elif extension == ".webp": + media_type = "image/webp" + + # Return file response + return FileResponse( + path=file_path, + media_type=media_type + ) + + +@router.get("/artist/image/{artist_id}") +async def get_artist_image( + artist_id: int, + db: Session = Depends(get_db), +) -> Any: + """ + Get artist image by artist ID. + """ + from app.services.artist import get_artist + + artist = get_artist(db, artist_id=artist_id) + if not artist or not artist.image_path: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Artist image not found", + ) + + # Construct file path + file_path = Path(artist.image_path) + + # Check if file exists + if not file_path.exists(): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Image file not found", + ) + + # Determine media type based on file extension + extension = file_path.suffix.lower() + media_type = "image/jpeg" # Default + if extension == ".png": + media_type = "image/png" + elif extension == ".gif": + media_type = "image/gif" + elif extension == ".webp": + media_type = "image/webp" + + # Return file response + return FileResponse( + path=file_path, + media_type=media_type + ) \ No newline at end of file diff --git a/app/api/v1/endpoints/upload.py b/app/api/v1/endpoints/upload.py new file mode 100644 index 0000000..af31993 --- /dev/null +++ b/app/api/v1/endpoints/upload.py @@ -0,0 +1,102 @@ +import os +import uuid +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form +from sqlalchemy.orm import Session + +from app.api.deps import get_db, get_current_active_superuser +from app.models.user import User +from app.core.config import settings + +router = APIRouter() + + +@router.post("/song", status_code=status.HTTP_201_CREATED) +async def upload_song( + *, + db: Session = Depends(get_db), + file: UploadFile = File(...), + current_user: User = Depends(get_current_active_superuser), +) -> Any: + """ + Upload a song file. Only superusers can upload songs. + """ + # Check file type + if not file.content_type.startswith("audio/"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File must be an audio file", + ) + + # Generate unique filename to avoid collisions + filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}" + + # Create directory if it doesn't exist + audio_dir = settings.AUDIO_DIR + audio_dir.mkdir(parents=True, exist_ok=True) + + # Save file + file_path = audio_dir / filename + + try: + contents = await file.read() + with open(file_path, "wb") as f: + f.write(contents) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error uploading file: {str(e)}", + ) + + # Return the file path (to be saved in Song model) + return {"file_path": str(file_path)} + + +@router.post("/image", status_code=status.HTTP_201_CREATED) +async def upload_image( + *, + db: Session = Depends(get_db), + file: UploadFile = File(...), + image_type: str = Form(...), # "artist" or "album" + current_user: User = Depends(get_current_active_superuser), +) -> Any: + """ + Upload an image file for artist or album. Only superusers can upload images. + """ + # Check file type + if not file.content_type.startswith("image/"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File must be an image", + ) + + # Check image type + if image_type not in ["artist", "album"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Image type must be 'artist' or 'album'", + ) + + # Generate unique filename to avoid collisions + filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}" + + # Create directory if it doesn't exist + images_dir = settings.IMAGES_DIR + images_dir.mkdir(parents=True, exist_ok=True) + + # Save file + file_path = images_dir / filename + + try: + contents = await file.read() + with open(file_path, "wb") as f: + f.write(contents) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error uploading file: {str(e)}", + ) + + # Return the file path (to be saved in Artist or Album model) + return {"file_path": str(file_path)} \ No newline at end of file