Files
LeDiscord/backend/api/routers/vlogs.py
EvanChal f33dfd5ab7
Some checks failed
Deploy to Development / build-and-deploy (push) Failing after 46s
Deploy to Production / build-and-deploy (push) Successful in 1m47s
fix(notification+vlog upload)
2026-01-27 02:39:51 +01:00

422 lines
14 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
from sqlalchemy.orm import Session
from typing import List
import os
import uuid
from pathlib import Path
from config.database import get_db
from config.settings import settings
from models.vlog import Vlog, VlogLike, VlogComment, VlogView
from models.user import User
from models.notification import Notification, NotificationType
from schemas.vlog import VlogCreate, VlogUpdate, VlogResponse, VlogCommentCreate
from utils.security import get_current_active_user
from utils.video_utils import generate_video_thumbnail, get_video_duration
from utils.settings_service import SettingsService
from utils.push_service import send_push_to_user
router = APIRouter()
@router.post("/", response_model=VlogResponse)
async def create_vlog(
vlog_data: VlogCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new vlog."""
vlog = Vlog(
author_id=current_user.id,
**vlog_data.dict()
)
db.add(vlog)
db.commit()
db.refresh(vlog)
return format_vlog_response(vlog, db, current_user.id)
@router.get("/", response_model=List[VlogResponse])
async def get_vlogs(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
limit: int = 20,
offset: int = 0
):
"""Get all vlogs."""
vlogs = db.query(Vlog).order_by(Vlog.created_at.desc()).limit(limit).offset(offset).all()
return [format_vlog_response(vlog, db, current_user.id) for vlog in vlogs]
@router.get("/{vlog_id}", response_model=VlogResponse)
async def get_vlog(
vlog_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
# Manage views and replays
view = db.query(VlogView).filter(
VlogView.vlog_id == vlog_id,
VlogView.user_id == current_user.id
).first()
if view:
# User has already viewed this vlog -> Count as replay
vlog.replays_count = (vlog.replays_count or 0) + 1
else:
# First time viewing -> Count as unique view
new_view = VlogView(vlog_id=vlog_id, user_id=current_user.id)
db.add(new_view)
vlog.views_count += 1
db.commit()
return format_vlog_response(vlog, db, current_user.id)
@router.put("/{vlog_id}", response_model=VlogResponse)
async def update_vlog(
vlog_id: int,
vlog_update: VlogUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
if vlog.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this vlog"
)
for field, value in vlog_update.dict(exclude_unset=True).items():
setattr(vlog, field, value)
db.commit()
db.refresh(vlog)
return format_vlog_response(vlog, db, current_user.id)
@router.delete("/{vlog_id}")
async def delete_vlog(
vlog_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
if vlog.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this vlog"
)
# Delete video files
try:
if vlog.video_url:
os.remove(settings.UPLOAD_PATH + vlog.video_url)
if vlog.thumbnail_url:
os.remove(settings.UPLOAD_PATH + vlog.thumbnail_url)
except:
pass
db.delete(vlog)
db.commit()
return {"message": "Vlog deleted successfully"}
@router.post("/{vlog_id}/like")
async def toggle_vlog_like(
vlog_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Toggle like on a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
existing_like = db.query(VlogLike).filter(
VlogLike.vlog_id == vlog_id,
VlogLike.user_id == current_user.id
).first()
if existing_like:
# Unlike
db.delete(existing_like)
vlog.likes_count = max(0, vlog.likes_count - 1)
message = "Like removed"
else:
# Like
try:
like = VlogLike(vlog_id=vlog_id, user_id=current_user.id)
db.add(like)
vlog.likes_count += 1
message = "Vlog liked"
except Exception:
# Handle potential race condition or constraint violation
db.rollback()
return {"message": "Already liked", "likes_count": vlog.likes_count}
db.commit()
return {"message": message, "likes_count": vlog.likes_count}
@router.post("/{vlog_id}/comment")
async def add_vlog_comment(
vlog_id: int,
comment_data: VlogCommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Add a comment to a vlog."""
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
if not vlog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Vlog not found"
)
comment = VlogComment(
vlog_id=vlog_id,
user_id=current_user.id,
content=comment_data.content
)
db.add(comment)
db.commit()
db.refresh(comment)
return {
"message": "Comment added successfully",
"comment": {
"id": comment.id,
"content": comment.content,
"user_id": comment.user_id,
"username": current_user.username,
"full_name": current_user.full_name,
"avatar_url": current_user.avatar_url,
"created_at": comment.created_at
}
}
@router.delete("/{vlog_id}/comment/{comment_id}")
async def delete_vlog_comment(
vlog_id: int,
comment_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a comment from a vlog."""
comment = db.query(VlogComment).filter(
VlogComment.id == comment_id,
VlogComment.vlog_id == vlog_id
).first()
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Comment not found"
)
if comment.user_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this comment"
)
db.delete(comment)
db.commit()
return {"message": "Comment deleted successfully"}
@router.post("/upload")
async def upload_vlog_video(
title: str = Form(...),
description: str = Form(None),
video: UploadFile = File(...),
thumbnail: UploadFile = File(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Upload a vlog video."""
# Validate video file
if not SettingsService.is_file_type_allowed(db, video.content_type or "unknown"):
# Fallback check for common video types if content_type is generic application/octet-stream
filename = video.filename.lower()
if not (filename.endswith('.mp4') or filename.endswith('.mov') or filename.endswith('.webm') or filename.endswith('.mkv')):
allowed_types = SettingsService.get_setting(db, "allowed_video_types",
["video/mp4", "video/mpeg", "video/quicktime", "video/webm"])
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid video type. Allowed types: {', '.join(allowed_types)}"
)
# Check file size
video_content = await video.read()
max_size = SettingsService.get_max_upload_size(db, video.content_type or "video/mp4", is_vlog=True)
if len(video_content) > max_size:
max_size_mb = max_size // (1024 * 1024)
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Video file too large. Maximum size: {max_size_mb}MB"
)
# Create vlog directory
vlog_dir = Path(settings.UPLOAD_PATH) / "vlogs" / str(current_user.id)
vlog_dir.mkdir(parents=True, exist_ok=True)
# Save video
video_extension = video.filename.split(".")[-1]
video_filename = f"{uuid.uuid4()}.{video_extension}"
video_path = vlog_dir / video_filename
with open(video_path, "wb") as f:
f.write(video_content)
# Process thumbnail
thumbnail_url = None
if thumbnail:
if thumbnail.content_type not in settings.ALLOWED_IMAGE_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid thumbnail type"
)
thumbnail_content = await thumbnail.read()
thumbnail_extension = thumbnail.filename.split(".")[-1]
thumbnail_filename = f"thumb_{uuid.uuid4()}.{thumbnail_extension}"
thumbnail_path = vlog_dir / thumbnail_filename
with open(thumbnail_path, "wb") as f:
f.write(thumbnail_content)
thumbnail_url = f"/vlogs/{current_user.id}/{thumbnail_filename}"
else:
# Generate automatic thumbnail from video
try:
thumbnail_filename = f"auto_thumb_{uuid.uuid4()}.jpg"
thumbnail_path = vlog_dir / thumbnail_filename
if generate_video_thumbnail(str(video_path), str(thumbnail_path)):
thumbnail_url = f"/vlogs/{current_user.id}/{thumbnail_filename}"
except Exception as e:
print(f"Error generating automatic thumbnail: {e}")
# Continue without thumbnail if generation fails
# Get video duration
duration = None
try:
duration = int(get_video_duration(str(video_path)))
except Exception as e:
print(f"Error getting video duration: {e}")
# Create vlog record
vlog = Vlog(
author_id=current_user.id,
title=title,
description=description,
video_url=f"/vlogs/{current_user.id}/{video_filename}",
thumbnail_url=thumbnail_url,
duration=duration
)
db.add(vlog)
db.commit()
db.refresh(vlog)
# Create notifications for all active users (except the creator)
users = db.query(User).filter(User.is_active == True).all()
notif_title = "Nouveau vlog"
notif_message = f"{current_user.full_name} a publié un nouveau vlog : {vlog.title}"
notif_link = f"/vlogs/{vlog.id}"
for user in users:
if user.id != current_user.id:
notification = Notification(
user_id=user.id,
type=NotificationType.NEW_VLOG,
title=notif_title,
message=notif_message,
link=notif_link,
is_read=False
)
db.add(notification)
# Envoyer la notification push
send_push_to_user(db, user.id, notif_title, notif_message, notif_link)
db.commit()
return format_vlog_response(vlog, db, current_user.id)
def format_vlog_response(vlog: Vlog, db: Session, current_user_id: int) -> dict:
"""Format vlog response with author information, likes, and comments."""
# Check if current user liked this vlog
is_liked = db.query(VlogLike).filter(
VlogLike.vlog_id == vlog.id,
VlogLike.user_id == current_user_id
).first() is not None
# Format likes
likes = []
for like in vlog.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
})
# Format comments
comments = []
for comment in vlog.comments:
user = db.query(User).filter(User.id == comment.user_id).first()
if user:
comments.append({
"id": comment.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"content": comment.content,
"created_at": comment.created_at,
"updated_at": comment.updated_at
})
return {
"id": vlog.id,
"author_id": vlog.author_id,
"title": vlog.title,
"description": vlog.description,
"video_url": vlog.video_url,
"thumbnail_url": vlog.thumbnail_url,
"duration": vlog.duration,
"views_count": vlog.views_count,
"replays_count": vlog.replays_count,
"likes_count": vlog.likes_count,
"created_at": vlog.created_at,
"updated_at": vlog.updated_at,
"author_name": vlog.author.full_name,
"author_avatar": vlog.author.avatar_url,
"is_liked": is_liked,
"likes": likes,
"comments": comments
}