initial commit - LeDiscord plateforme des copains
This commit is contained in:
464
backend/api/routers/albums.py
Normal file
464
backend/api/routers/albums.py
Normal file
@@ -0,0 +1,464 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import magic
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.album import Album, Media, MediaType, MediaLike
|
||||
from models.user import User
|
||||
from schemas.album import AlbumCreate, AlbumUpdate, AlbumResponse
|
||||
from utils.security import get_current_active_user
|
||||
from utils.settings_service import SettingsService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=AlbumResponse)
|
||||
async def create_album(
|
||||
album_data: AlbumCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new album."""
|
||||
album = Album(
|
||||
**album_data.dict(),
|
||||
creator_id=current_user.id
|
||||
)
|
||||
db.add(album)
|
||||
db.commit()
|
||||
db.refresh(album)
|
||||
return format_album_response(album, db, current_user.id)
|
||||
|
||||
@router.get("/", response_model=List[AlbumResponse])
|
||||
async def get_albums(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
event_id: Optional[int] = None
|
||||
):
|
||||
"""Get all albums, optionally filtered by event."""
|
||||
query = db.query(Album)
|
||||
if event_id:
|
||||
query = query.filter(Album.event_id == event_id)
|
||||
albums = query.order_by(Album.created_at.desc()).all()
|
||||
return [format_album_response(album, db, current_user.id) for album in albums]
|
||||
|
||||
@router.get("/{album_id}", response_model=AlbumResponse)
|
||||
async def get_album(
|
||||
album_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
return format_album_response(album, db, current_user.id)
|
||||
|
||||
@router.put("/{album_id}", response_model=AlbumResponse)
|
||||
async def update_album(
|
||||
album_id: int,
|
||||
album_update: AlbumUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update an album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this album"
|
||||
)
|
||||
|
||||
for field, value in album_update.dict(exclude_unset=True).items():
|
||||
setattr(album, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(album)
|
||||
return format_album_response(album, db, current_user.id)
|
||||
|
||||
@router.delete("/{album_id}")
|
||||
async def delete_album(
|
||||
album_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete an album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this album"
|
||||
)
|
||||
|
||||
# Delete media files
|
||||
for media in album.media:
|
||||
try:
|
||||
if media.file_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.file_path)
|
||||
if media.thumbnail_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.thumbnail_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
db.delete(album)
|
||||
db.commit()
|
||||
return {"message": "Album deleted successfully"}
|
||||
|
||||
@router.post("/{album_id}/media")
|
||||
async def upload_media(
|
||||
album_id: int,
|
||||
files: List[UploadFile] = File(...),
|
||||
captions: Optional[List[str]] = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Upload media files to an album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to add media to this album"
|
||||
)
|
||||
|
||||
# Check max media per album limit
|
||||
from utils.settings_service import SettingsService
|
||||
max_media_per_album = SettingsService.get_setting(db, "max_media_per_album", 50)
|
||||
current_media_count = len(album.media)
|
||||
|
||||
if current_media_count + len(files) > max_media_per_album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Album would exceed maximum of {max_media_per_album} media files. Current: {current_media_count}, trying to add: {len(files)}"
|
||||
)
|
||||
|
||||
# Create album directory
|
||||
album_dir = Path(settings.UPLOAD_PATH) / "albums" / str(album_id)
|
||||
album_dir.mkdir(parents=True, exist_ok=True)
|
||||
thumbnails_dir = album_dir / "thumbnails"
|
||||
thumbnails_dir.mkdir(exist_ok=True)
|
||||
|
||||
uploaded_media = []
|
||||
|
||||
for i, file in enumerate(files):
|
||||
# Check file size
|
||||
contents = await file.read()
|
||||
file_size = len(contents)
|
||||
|
||||
# Get dynamic upload limits
|
||||
max_size = SettingsService.get_max_upload_size(db, file.content_type or "unknown")
|
||||
if file_size > max_size:
|
||||
max_size_mb = max_size // (1024 * 1024)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File {file.filename} exceeds maximum size of {max_size_mb}MB"
|
||||
)
|
||||
|
||||
# Check file type
|
||||
mime = magic.from_buffer(contents, mime=True)
|
||||
if not SettingsService.is_file_type_allowed(db, mime):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File {file.filename} has unsupported type: {mime}"
|
||||
)
|
||||
|
||||
# Determine media type
|
||||
if mime.startswith('image/'):
|
||||
media_type = MediaType.IMAGE
|
||||
elif mime.startswith('video/'):
|
||||
media_type = MediaType.VIDEO
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File {file.filename} has unsupported type: {mime}"
|
||||
)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = file.filename.split(".")[-1]
|
||||
filename = f"{uuid.uuid4()}.{file_extension}"
|
||||
file_path = album_dir / filename
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Process media
|
||||
thumbnail_path = None
|
||||
width = height = duration = None
|
||||
|
||||
if media_type == MediaType.IMAGE:
|
||||
# Get image dimensions and create thumbnail
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
width, height = img.size
|
||||
|
||||
# Create thumbnail with better quality
|
||||
thumbnail = img.copy()
|
||||
thumbnail.thumbnail((800, 800)) # Larger thumbnail for better quality
|
||||
thumbnail_filename = f"thumb_{filename}"
|
||||
thumbnail_path_full = thumbnails_dir / thumbnail_filename
|
||||
thumbnail.save(thumbnail_path_full, quality=95, optimize=True) # High quality
|
||||
thumbnail_path = f"/albums/{album_id}/thumbnails/{thumbnail_filename}"
|
||||
except Exception as e:
|
||||
print(f"Error processing image: {e}")
|
||||
|
||||
# Create media record
|
||||
media = Media(
|
||||
album_id=album_id,
|
||||
file_path=f"/albums/{album_id}/{filename}",
|
||||
thumbnail_path=thumbnail_path,
|
||||
media_type=media_type,
|
||||
caption=captions[i] if captions and i < len(captions) else None,
|
||||
file_size=file_size,
|
||||
width=width,
|
||||
height=height,
|
||||
duration=duration
|
||||
)
|
||||
db.add(media)
|
||||
uploaded_media.append(media)
|
||||
|
||||
# Set first image as album cover if not set
|
||||
if not album.cover_image and uploaded_media:
|
||||
first_image = next((m for m in uploaded_media if m.media_type == MediaType.IMAGE), None)
|
||||
if first_image:
|
||||
album.cover_image = first_image.thumbnail_path or first_image.file_path
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update event cover image if this album is linked to an event
|
||||
if album.event_id:
|
||||
update_event_cover_from_album(album.event_id, db)
|
||||
|
||||
return {
|
||||
"message": f"Successfully uploaded {len(uploaded_media)} files",
|
||||
"media_count": len(uploaded_media)
|
||||
}
|
||||
|
||||
@router.delete("/{album_id}/media/{media_id}")
|
||||
async def delete_media(
|
||||
album_id: int,
|
||||
media_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a media file from an album."""
|
||||
media = db.query(Media).filter(
|
||||
Media.id == media_id,
|
||||
Media.album_id == album_id
|
||||
).first()
|
||||
|
||||
if not media:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Media not found"
|
||||
)
|
||||
|
||||
album = media.album
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this media"
|
||||
)
|
||||
|
||||
# Delete files
|
||||
try:
|
||||
if media.file_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.file_path)
|
||||
if media.thumbnail_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.thumbnail_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
|
||||
# Update event cover image if this album is linked to an event
|
||||
if album.event_id:
|
||||
update_event_cover_from_album(album.event_id, db)
|
||||
|
||||
return {"message": "Media deleted successfully"}
|
||||
|
||||
@router.post("/{album_id}/media/{media_id}/like")
|
||||
async def toggle_media_like(
|
||||
album_id: int,
|
||||
media_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Toggle like on a media file."""
|
||||
media = db.query(Media).filter(
|
||||
Media.id == media_id,
|
||||
Media.album_id == album_id
|
||||
).first()
|
||||
|
||||
if not media:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Media not found"
|
||||
)
|
||||
|
||||
existing_like = db.query(MediaLike).filter(
|
||||
MediaLike.media_id == media_id,
|
||||
MediaLike.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# Unlike
|
||||
db.delete(existing_like)
|
||||
media.likes_count = max(0, media.likes_count - 1)
|
||||
message = "Like removed"
|
||||
else:
|
||||
# Like
|
||||
like = MediaLike(media_id=media_id, user_id=current_user.id)
|
||||
db.add(like)
|
||||
media.likes_count += 1
|
||||
message = "Media liked"
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update event cover image if this album is linked to an event
|
||||
album = media.album
|
||||
if album.event_id:
|
||||
update_event_cover_from_album(album.event_id, db)
|
||||
|
||||
return {"message": message, "likes_count": media.likes_count}
|
||||
|
||||
def format_album_response(album: Album, db: Session, current_user_id: int) -> dict:
|
||||
"""Format album response with additional information."""
|
||||
# Get top media (most liked)
|
||||
top_media = db.query(Media).filter(
|
||||
Media.album_id == album.id
|
||||
).order_by(Media.likes_count.desc()).limit(3).all()
|
||||
|
||||
# Format media with likes information
|
||||
formatted_media = []
|
||||
for media in album.media:
|
||||
# Check if current user liked this media
|
||||
is_liked = db.query(MediaLike).filter(
|
||||
MediaLike.media_id == media.id,
|
||||
MediaLike.user_id == current_user_id
|
||||
).first() is not None
|
||||
|
||||
# Format likes
|
||||
likes = []
|
||||
for like in media.likes:
|
||||
user = db.query(User).filter(User.id == like.user_id).first()
|
||||
if user:
|
||||
likes.append({
|
||||
"id": like.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"created_at": like.created_at
|
||||
})
|
||||
|
||||
formatted_media.append({
|
||||
"id": media.id,
|
||||
"file_path": media.file_path,
|
||||
"thumbnail_path": media.thumbnail_path,
|
||||
"media_type": media.media_type,
|
||||
"caption": media.caption,
|
||||
"file_size": media.file_size,
|
||||
"width": media.width,
|
||||
"height": media.height,
|
||||
"duration": media.duration,
|
||||
"likes_count": media.likes_count,
|
||||
"is_liked": is_liked,
|
||||
"likes": likes,
|
||||
"created_at": media.created_at
|
||||
})
|
||||
|
||||
# Format top media
|
||||
formatted_top_media = []
|
||||
for media in top_media:
|
||||
is_liked = db.query(MediaLike).filter(
|
||||
MediaLike.media_id == media.id,
|
||||
MediaLike.user_id == current_user_id
|
||||
).first() is not None
|
||||
|
||||
likes = []
|
||||
for like in media.likes:
|
||||
user = db.query(User).filter(User.id == like.user_id).first()
|
||||
if user:
|
||||
likes.append({
|
||||
"id": like.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"created_at": like.created_at
|
||||
})
|
||||
|
||||
formatted_top_media.append({
|
||||
"id": media.id,
|
||||
"file_path": media.file_path,
|
||||
"thumbnail_path": media.thumbnail_path,
|
||||
"media_type": media.media_type,
|
||||
"caption": media.caption,
|
||||
"file_size": media.file_size,
|
||||
"width": media.width,
|
||||
"height": media.height,
|
||||
"duration": media.duration,
|
||||
"likes_count": media.likes_count,
|
||||
"is_liked": is_liked,
|
||||
"likes": likes,
|
||||
"created_at": media.created_at
|
||||
})
|
||||
|
||||
return {
|
||||
"id": album.id,
|
||||
"title": album.title,
|
||||
"description": album.description,
|
||||
"creator_id": album.creator_id,
|
||||
"event_id": album.event_id,
|
||||
"cover_image": album.cover_image,
|
||||
"created_at": album.created_at,
|
||||
"updated_at": album.updated_at,
|
||||
"creator_name": album.creator.full_name,
|
||||
"media_count": len(album.media),
|
||||
"media": formatted_media,
|
||||
"top_media": formatted_top_media,
|
||||
"event_title": album.event.title if album.event else None
|
||||
}
|
||||
|
||||
def update_event_cover_from_album(event_id: int, db: Session):
|
||||
"""Update event cover image from the most liked media in linked albums."""
|
||||
from models.event import Event
|
||||
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
return
|
||||
|
||||
# Get the most liked media from all albums linked to this event
|
||||
most_liked_media = db.query(Media).join(Album).filter(
|
||||
Album.event_id == event_id,
|
||||
Media.media_type == MediaType.IMAGE
|
||||
).order_by(Media.likes_count.desc()).first()
|
||||
|
||||
if most_liked_media:
|
||||
event.cover_image = most_liked_media.thumbnail_path or most_liked_media.file_path
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user