Initial Animatrix import
This commit is contained in:
118
backend/app/services/storage.py
Normal file
118
backend/app/services/storage.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import subprocess
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
from fastapi import UploadFile
|
||||
from PIL import Image
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class LocalStorageService:
|
||||
def __init__(self, root: str):
|
||||
self.root = Path(root)
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def save_upload(self, upload: UploadFile, subfolder: str) -> tuple[str, int]:
|
||||
dest_dir = self.root / subfolder
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
ext = Path(upload.filename or "file").suffix
|
||||
filename = f"{uuid.uuid4()}{ext}"
|
||||
dest_path = dest_dir / filename
|
||||
content = await upload.read()
|
||||
async with aiofiles.open(dest_path, "wb") as handle:
|
||||
await handle.write(content)
|
||||
return str(dest_path.relative_to(self.root)).replace("\\", "/"), len(content)
|
||||
|
||||
def save_bytes(self, data: bytes, subfolder: str, filename: str) -> str:
|
||||
dest_dir = self.root / subfolder
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest_path = dest_dir / filename
|
||||
with open(dest_path, "wb") as handle:
|
||||
handle.write(data)
|
||||
return str(dest_path.relative_to(self.root)).replace("\\", "/")
|
||||
|
||||
def absolute_path(self, relative_path: str) -> Path:
|
||||
return self.root / relative_path
|
||||
|
||||
def delete_relative_path(self, relative_path: Optional[str]) -> None:
|
||||
if not relative_path:
|
||||
return
|
||||
abs_path = self.absolute_path(relative_path)
|
||||
try:
|
||||
if abs_path.exists():
|
||||
abs_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def generate_thumbnail(self, image_path: str, thumb_subfolder: str) -> Optional[str]:
|
||||
try:
|
||||
abs_path = self.absolute_path(image_path)
|
||||
with Image.open(abs_path) as img:
|
||||
img.thumbnail((400, 400))
|
||||
thumb_dir = self.root / thumb_subfolder
|
||||
thumb_dir.mkdir(parents=True, exist_ok=True)
|
||||
thumb_name = f"thumb_{Path(image_path).stem}.jpg"
|
||||
thumb_path = thumb_dir / thumb_name
|
||||
img.convert("RGB").save(thumb_path, "JPEG", quality=80)
|
||||
return str(thumb_path.relative_to(self.root)).replace("\\", "/")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def generate_video_thumbnail(self, video_path: str, thumb_subfolder: str) -> Optional[str]:
|
||||
abs_path = self.absolute_path(video_path)
|
||||
thumb_dir = self.root / thumb_subfolder
|
||||
thumb_dir.mkdir(parents=True, exist_ok=True)
|
||||
thumb_name = f"thumb_{Path(video_path).stem}.jpg"
|
||||
thumb_path = thumb_dir / thumb_name
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-i",
|
||||
str(abs_path),
|
||||
"-ss",
|
||||
"00:00:00.500",
|
||||
"-vframes",
|
||||
"1",
|
||||
str(thumb_path),
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
if thumb_path.exists():
|
||||
return str(thumb_path.relative_to(self.root)).replace("\\", "/")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def detect_duration_seconds(self, relative_path: str) -> Optional[float]:
|
||||
abs_path = self.absolute_path(relative_path)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
str(abs_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=20,
|
||||
check=True,
|
||||
)
|
||||
return round(float(result.stdout.strip()), 3)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
asset_storage = LocalStorageService(settings.ASSET_STORAGE_ROOT)
|
||||
output_storage = LocalStorageService(settings.OUTPUT_STORAGE_ROOT)
|
||||
Reference in New Issue
Block a user