fix(video-player): fix the video player to permit the nagivation through the video (it was because de fast api server refused range request)
Some checks failed
Deploy to Development / build-and-deploy (push) Failing after 2m15s
Some checks failed
Deploy to Development / build-and-deploy (push) Failing after 2m15s
This commit is contained in:
@@ -6,7 +6,7 @@ import uuid
|
||||
from pathlib import Path
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.vlog import Vlog, VlogLike, VlogComment
|
||||
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
|
||||
@@ -56,8 +56,21 @@ async def get_vlog(
|
||||
detail="Vlog not found"
|
||||
)
|
||||
|
||||
# Increment view count
|
||||
vlog.views_count += 1
|
||||
# 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)
|
||||
@@ -149,10 +162,15 @@ async def toggle_vlog_like(
|
||||
message = "Like removed"
|
||||
else:
|
||||
# Like
|
||||
like = VlogLike(vlog_id=vlog_id, user_id=current_user.id)
|
||||
db.add(like)
|
||||
vlog.likes_count += 1
|
||||
message = "Vlog liked"
|
||||
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}
|
||||
@@ -244,7 +262,7 @@ async def upload_vlog_video(
|
||||
|
||||
# Check file size
|
||||
video_content = await video.read()
|
||||
max_size = SettingsService.get_max_upload_size(db, video.content_type or "video/mp4")
|
||||
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(
|
||||
@@ -363,6 +381,7 @@ def format_vlog_response(vlog: Vlog, db: Session, current_user_id: int) -> dict:
|
||||
"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,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request, Response, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import mimetypes
|
||||
|
||||
from config.settings import settings
|
||||
from config.database import engine, Base
|
||||
@@ -198,10 +199,89 @@ app.add_middleware(
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["Content-Range", "Accept-Ranges"],
|
||||
)
|
||||
|
||||
# Mount static files for uploads
|
||||
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_PATH), name="uploads")
|
||||
# Endpoint personnalisé pour servir les vidéos avec support Range
|
||||
@app.get("/uploads/{file_path:path}")
|
||||
async def serve_media_with_range(request: Request, file_path: str):
|
||||
"""
|
||||
Serve media files with proper Range request support for video scrubbing.
|
||||
"""
|
||||
file_full_path = Path(settings.UPLOAD_PATH) / file_path
|
||||
|
||||
# Vérifier que le fichier existe
|
||||
if not file_full_path.exists() or not file_full_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Vérifier que le fichier est dans le répertoire uploads (sécurité)
|
||||
try:
|
||||
file_full_path.resolve().relative_to(Path(settings.UPLOAD_PATH).resolve())
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Obtenir la taille du fichier
|
||||
file_size = file_full_path.stat().st_size
|
||||
|
||||
# Déterminer le content type
|
||||
content_type, _ = mimetypes.guess_type(str(file_full_path))
|
||||
if not content_type:
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
# Gérer les requêtes Range
|
||||
range_header = request.headers.get("Range")
|
||||
|
||||
if range_header:
|
||||
# Parser le header Range (format: bytes=start-end)
|
||||
range_match = range_header.replace("bytes=", "").split("-")
|
||||
start = int(range_match[0]) if range_match[0] else 0
|
||||
end = int(range_match[1]) if range_match[1] and range_match[1] else file_size - 1
|
||||
|
||||
# Valider la plage
|
||||
if start >= file_size or end >= file_size or start > end:
|
||||
return Response(
|
||||
status_code=416,
|
||||
headers={
|
||||
"Content-Range": f"bytes */{file_size}",
|
||||
"Accept-Ranges": "bytes"
|
||||
}
|
||||
)
|
||||
|
||||
# Lire la plage demandée
|
||||
chunk_size = end - start + 1
|
||||
with open(file_full_path, "rb") as f:
|
||||
f.seek(start)
|
||||
chunk = f.read(chunk_size)
|
||||
|
||||
# Retourner la réponse 206 Partial Content
|
||||
return Response(
|
||||
content=chunk,
|
||||
status_code=206,
|
||||
headers={
|
||||
"Content-Range": f"bytes {start}-{end}/{file_size}",
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Length": str(chunk_size),
|
||||
"Content-Type": content_type,
|
||||
},
|
||||
media_type=content_type
|
||||
)
|
||||
else:
|
||||
# Pas de Range header, retourner le fichier complet
|
||||
with open(file_full_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
return Response(
|
||||
content=content,
|
||||
headers={
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Length": str(file_size),
|
||||
"Content-Type": content_type,
|
||||
},
|
||||
media_type=content_type
|
||||
)
|
||||
|
||||
# Note: StaticFiles mount retiré car notre endpoint personnalisé gère tous les fichiers
|
||||
# avec support Range pour permettre le scrubbing vidéo
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from config.database import Base
|
||||
@@ -14,6 +14,7 @@ class Vlog(Base):
|
||||
thumbnail_url = Column(String)
|
||||
duration = Column(Integer) # in seconds
|
||||
views_count = Column(Integer, default=0)
|
||||
replays_count = Column(Integer, default=0)
|
||||
likes_count = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@@ -35,6 +36,10 @@ class VlogLike(Base):
|
||||
vlog = relationship("Vlog", back_populates="likes")
|
||||
user = relationship("User", back_populates="vlog_likes")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('vlog_id', 'user_id', name='_vlog_user_like_uc'),
|
||||
)
|
||||
|
||||
class VlogComment(Base):
|
||||
__tablename__ = "vlog_comments"
|
||||
|
||||
@@ -48,3 +53,19 @@ class VlogComment(Base):
|
||||
# Relationships
|
||||
vlog = relationship("Vlog", back_populates="comments")
|
||||
user = relationship("User", back_populates="vlog_comments")
|
||||
|
||||
class VlogView(Base):
|
||||
__tablename__ = "vlog_views"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vlog_id = Column(Integer, ForeignKey("vlogs.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
vlog = relationship("Vlog")
|
||||
user = relationship("User")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('vlog_id', 'user_id', name='_vlog_user_view_uc'),
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ class VlogResponse(VlogBase):
|
||||
thumbnail_url: Optional[str]
|
||||
duration: Optional[int]
|
||||
views_count: int
|
||||
replays_count: int = 0
|
||||
likes_count: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
from sqlalchemy.orm import Session
|
||||
from models.settings import SystemSettings
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
|
||||
class SettingsService:
|
||||
"""Service for managing system settings."""
|
||||
|
||||
@staticmethod
|
||||
def get_setting(db: Session, key: str, default: Any = None) -> Any:
|
||||
"""Get a setting value by key."""
|
||||
"""Get a setting value by key, return default if not found."""
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if not setting:
|
||||
return default
|
||||
|
||||
# Essayer de convertir en type approprié
|
||||
value = setting.value
|
||||
|
||||
# Booléens
|
||||
if value.lower() in ['true', 'false']:
|
||||
return value.lower() == 'true'
|
||||
|
||||
# Nombres entiers
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Nombres flottants
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# JSON
|
||||
if value.startswith('{') or value.startswith('['):
|
||||
|
||||
# Convert value based on expected type (basic handling)
|
||||
if isinstance(default, int):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return int(setting.value)
|
||||
except ValueError:
|
||||
return default
|
||||
elif isinstance(default, bool):
|
||||
return setting.value.lower() == "true"
|
||||
elif isinstance(default, list):
|
||||
return setting.value.split(",")
|
||||
|
||||
return setting.value
|
||||
|
||||
@staticmethod
|
||||
def set_setting(db: Session, key: str, value: str, description: str = None, category: str = "general") -> SystemSettings:
|
||||
"""Set a setting value."""
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if setting:
|
||||
setting.value = str(value)
|
||||
if description:
|
||||
setting.description = description
|
||||
if category:
|
||||
setting.category = category
|
||||
else:
|
||||
setting = SystemSettings(
|
||||
key=key,
|
||||
value=str(value),
|
||||
description=description,
|
||||
category=category
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Liste séparée par des virgules
|
||||
if ',' in value and not value.startswith('{') and not value.startswith('['):
|
||||
return [item.strip() for item in value.split(',')]
|
||||
|
||||
return value
|
||||
db.commit()
|
||||
db.refresh(setting)
|
||||
return setting
|
||||
|
||||
@staticmethod
|
||||
def get_upload_limits(db: Session) -> Dict[str, Any]:
|
||||
@@ -60,7 +64,7 @@ class SettingsService:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_max_upload_size(db: Session, content_type: str) -> int:
|
||||
def get_max_upload_size(db: Session, content_type: str, is_vlog: bool = False) -> int:
|
||||
"""Get max upload size for a specific content type."""
|
||||
if content_type.startswith('image/'):
|
||||
max_size_mb = SettingsService.get_setting(db, "max_image_size_mb", 10)
|
||||
@@ -68,9 +72,14 @@ class SettingsService:
|
||||
print(f"DEBUG - Image upload limit: {max_size_mb}MB = {max_size_bytes} bytes")
|
||||
return max_size_bytes
|
||||
elif content_type.startswith('video/'):
|
||||
max_size_mb = SettingsService.get_setting(db, "max_video_size_mb", 100)
|
||||
if is_vlog:
|
||||
max_size_mb = SettingsService.get_setting(db, "max_vlog_size_mb", 500)
|
||||
print(f"DEBUG - Vlog upload limit: {max_size_mb}MB")
|
||||
else:
|
||||
max_size_mb = SettingsService.get_setting(db, "max_video_size_mb", 100)
|
||||
print(f"DEBUG - Video upload limit: {max_size_mb}MB")
|
||||
|
||||
max_size_bytes = max_size_mb * 1024 * 1024
|
||||
print(f"DEBUG - Video upload limit: {max_size_mb}MB = {max_size_bytes} bytes")
|
||||
return max_size_bytes
|
||||
else:
|
||||
default_size = 10 * 1024 * 1024 # 10MB par défaut
|
||||
@@ -79,14 +88,17 @@ class SettingsService:
|
||||
|
||||
@staticmethod
|
||||
def is_file_type_allowed(db: Session, content_type: str) -> bool:
|
||||
"""Check if a file type is allowed."""
|
||||
"""Check if file type is allowed."""
|
||||
if content_type.startswith('image/'):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_image_types",
|
||||
allowed = SettingsService.get_setting(db, "allowed_image_types",
|
||||
["image/jpeg", "image/png", "image/gif", "image/webp"])
|
||||
if isinstance(allowed, str):
|
||||
allowed = allowed.split(",")
|
||||
return content_type in allowed
|
||||
elif content_type.startswith('video/'):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_video_types",
|
||||
allowed = SettingsService.get_setting(db, "allowed_video_types",
|
||||
["video/mp4", "video/mpeg", "video/quicktime", "video/webm"])
|
||||
else:
|
||||
return False
|
||||
|
||||
return content_type in allowed_types
|
||||
if isinstance(allowed, str):
|
||||
allowed = allowed.split(",")
|
||||
return content_type in allowed
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user