169 lines
5.5 KiB
Python
Executable File
169 lines
5.5 KiB
Python
Executable File
"""
|
|
Storage Service
|
|
Handles file storage for uploaded and converted videos
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from app.config import Settings, get_settings
|
|
from app.models.schemas import VideoMetadata, VideoStatus
|
|
|
|
|
|
class StorageService:
|
|
"""Manage video file storage."""
|
|
|
|
def __init__(self, settings: Settings | None = None):
|
|
self.settings = settings or get_settings()
|
|
self.base_path = Path(self.settings.storage_path)
|
|
self.uploads_path = self.base_path / "uploads"
|
|
self.converted_path = self.base_path / "converted"
|
|
|
|
# Ensure directories exist
|
|
self.uploads_path.mkdir(parents=True, exist_ok=True)
|
|
self.converted_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
def generate_video_id(self) -> str:
|
|
"""Generate unique video ID."""
|
|
return str(uuid.uuid4())[:12]
|
|
|
|
def get_upload_path(self, video_id: str, filename: str) -> Path:
|
|
"""Get path for uploaded video file."""
|
|
video_dir = self.uploads_path / video_id
|
|
video_dir.mkdir(parents=True, exist_ok=True)
|
|
return video_dir / filename
|
|
|
|
def get_converted_path(self, video_id: str) -> Path:
|
|
"""Get path for converted video directory."""
|
|
video_dir = self.converted_path / video_id
|
|
video_dir.mkdir(parents=True, exist_ok=True)
|
|
return video_dir
|
|
|
|
def get_hls_path(self, video_id: str) -> Path:
|
|
"""Get path for HLS output."""
|
|
return self.get_converted_path(video_id) / "hls"
|
|
|
|
def get_thumbnail_path(self, video_id: str) -> Path:
|
|
"""Get path for thumbnail."""
|
|
return self.get_converted_path(video_id) / "thumbnail.jpg"
|
|
|
|
def get_metadata_path(self, video_id: str) -> Path:
|
|
"""Get path for metadata JSON."""
|
|
return self.get_converted_path(video_id) / "metadata.json"
|
|
|
|
def save_metadata(
|
|
self,
|
|
video_id: str,
|
|
kurs_id: int,
|
|
title: str,
|
|
status: VideoStatus,
|
|
metadata: VideoMetadata | None = None,
|
|
error_message: str | None = None,
|
|
) -> dict:
|
|
"""Save video metadata to JSON file."""
|
|
meta_path = self.get_metadata_path(video_id)
|
|
|
|
data = {
|
|
"video_id": video_id,
|
|
"kurs_id": kurs_id,
|
|
"title": title,
|
|
"status": status.value,
|
|
"created_at": datetime.now(UTC).isoformat(),
|
|
"updated_at": datetime.now(UTC).isoformat(),
|
|
"error_message": error_message,
|
|
}
|
|
|
|
if metadata:
|
|
data["metadata"] = metadata.model_dump()
|
|
|
|
# Load existing data to preserve created_at
|
|
if meta_path.exists():
|
|
with meta_path.open() as f:
|
|
existing = json.load(f)
|
|
data["created_at"] = existing.get("created_at", data["created_at"])
|
|
|
|
with meta_path.open("w") as f:
|
|
json.dump(data, f, indent=2, default=str)
|
|
|
|
return data
|
|
|
|
def load_metadata(self, video_id: str) -> dict | None:
|
|
"""Load video metadata from JSON file."""
|
|
meta_path = self.get_metadata_path(video_id)
|
|
if not meta_path.exists():
|
|
return None
|
|
|
|
with meta_path.open() as f:
|
|
return json.load(f)
|
|
|
|
def update_status(
|
|
self,
|
|
video_id: str,
|
|
status: VideoStatus,
|
|
progress: int = 0,
|
|
error_message: str | None = None,
|
|
) -> dict | None:
|
|
"""Update video status in metadata."""
|
|
meta = self.load_metadata(video_id)
|
|
if not meta:
|
|
return None
|
|
|
|
meta["status"] = status.value
|
|
meta["progress"] = progress
|
|
meta["updated_at"] = datetime.now(UTC).isoformat()
|
|
if error_message:
|
|
meta["error_message"] = error_message
|
|
|
|
meta_path = self.get_metadata_path(video_id)
|
|
with meta_path.open("w") as f:
|
|
json.dump(meta, f, indent=2, default=str)
|
|
|
|
return meta
|
|
|
|
def delete_video(self, video_id: str) -> bool:
|
|
"""Delete all files for a video."""
|
|
upload_dir = self.uploads_path / video_id
|
|
converted_dir = self.converted_path / video_id
|
|
|
|
deleted = False
|
|
if upload_dir.exists():
|
|
shutil.rmtree(upload_dir)
|
|
deleted = True
|
|
if converted_dir.exists():
|
|
shutil.rmtree(converted_dir)
|
|
deleted = True
|
|
|
|
return deleted
|
|
|
|
def get_video_ids(self) -> list[str]:
|
|
"""Get all video IDs."""
|
|
ids = set()
|
|
if self.uploads_path.exists():
|
|
ids.update(d.name for d in self.uploads_path.iterdir() if d.is_dir())
|
|
if self.converted_path.exists():
|
|
ids.update(d.name for d in self.converted_path.iterdir() if d.is_dir())
|
|
return sorted(ids)
|
|
|
|
def is_writable(self) -> bool:
|
|
"""Check if storage is writable."""
|
|
test_file = self.base_path / ".write_test"
|
|
try:
|
|
test_file.write_text("test")
|
|
test_file.unlink()
|
|
return True
|
|
except (OSError, PermissionError):
|
|
return False
|
|
|
|
def get_disk_usage(self) -> dict:
|
|
"""Get disk usage statistics."""
|
|
total, used, free = shutil.disk_usage(self.base_path)
|
|
return {
|
|
"total_gb": round(total / (1024**3), 2),
|
|
"used_gb": round(used / (1024**3), 2),
|
|
"free_gb": round(free / (1024**3), 2),
|
|
"percent_used": round((used / total) * 100, 1),
|
|
}
|