394 lines
13 KiB
Python
394 lines
13 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 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
|
|
|
|
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"):
|
|
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)
|
|
|
|
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
|
|
}
|