initial commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
File Server API for Meta-Stremio
|
||||
|
||||
Serves files by CID by looking up the path from metadata.
|
||||
This is a generic file serving endpoint that can serve any file
|
||||
whose CID and path are stored in the metadata.
|
||||
|
||||
Endpoints:
|
||||
- GET /file/{cid} - Serve file by CID
|
||||
- GET /file/{cid}/w{width} - Serve image resized to width (images only)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import io
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Optional PIL for image resizing
|
||||
try:
|
||||
from PIL import Image
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
Image = None
|
||||
|
||||
# Configuration
|
||||
FILES_PATH = os.environ.get('FILES_PATH', '/files')
|
||||
|
||||
# Storage reference (set by init)
|
||||
_storage = None
|
||||
|
||||
|
||||
def init(storage) -> None:
|
||||
"""Initialize with storage reference."""
|
||||
global _storage
|
||||
_storage = storage
|
||||
|
||||
|
||||
def lookup_path_by_cid(cid: str) -> Optional[str]:
|
||||
"""
|
||||
Look up a file path by its CID from Redis.
|
||||
|
||||
Files (including poster images) are stored in Redis as file:{cid} entries.
|
||||
The file path is stored in the 'path' field.
|
||||
|
||||
Returns the relative path if found, None otherwise.
|
||||
"""
|
||||
if not _storage:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Look up directly by CID - poster files are processed like any other file
|
||||
file_path = _storage.get_file_path_by_cid(cid)
|
||||
if file_path:
|
||||
return file_path
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[FileServer] Error looking up CID {cid}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_file_path(cid: str) -> Optional[str]:
|
||||
"""Get the absolute file path for a CID."""
|
||||
rel_path = lookup_path_by_cid(cid)
|
||||
if rel_path:
|
||||
return os.path.join(FILES_PATH, rel_path)
|
||||
return None
|
||||
|
||||
|
||||
def resize_image(image_data: bytes, width: int) -> bytes:
|
||||
"""Resize an image to the specified width, maintaining aspect ratio."""
|
||||
if not PIL_AVAILABLE:
|
||||
raise RuntimeError("PIL not available for image resizing")
|
||||
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
|
||||
# Calculate new height maintaining aspect ratio
|
||||
ratio = width / img.width
|
||||
height = int(img.height * ratio)
|
||||
|
||||
# Use high-quality resampling
|
||||
resized = img.resize((width, height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Save to bytes
|
||||
output = io.BytesIO()
|
||||
# Preserve format, default to JPEG
|
||||
fmt = img.format or 'JPEG'
|
||||
if fmt.upper() == 'PNG' and img.mode == 'RGBA':
|
||||
resized.save(output, format='PNG', optimize=True)
|
||||
else:
|
||||
# Convert to RGB for JPEG
|
||||
if resized.mode in ('RGBA', 'P'):
|
||||
resized = resized.convert('RGB')
|
||||
resized.save(output, format='JPEG', quality=85, optimize=True)
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def serve_file(cid: str, width: Optional[int] = None) -> Tuple[Optional[bytes], str, int]:
|
||||
"""
|
||||
Serve a file by CID.
|
||||
|
||||
Args:
|
||||
cid: The CID of the file
|
||||
width: Optional width for resizing (images only)
|
||||
|
||||
Returns:
|
||||
Tuple of (file_data, content_type, status_code)
|
||||
"""
|
||||
# Get file path from CID
|
||||
file_path = get_file_path(cid)
|
||||
|
||||
if not file_path:
|
||||
return None, 'text/plain', 404
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"[FileServer] File not found: {file_path}")
|
||||
return None, 'text/plain', 404
|
||||
|
||||
# Determine content type from extension
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
content_types = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.webp': 'image/webp',
|
||||
'.gif': 'image/gif',
|
||||
'.mp4': 'video/mp4',
|
||||
'.mkv': 'video/x-matroska',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.webm': 'video/webm',
|
||||
}
|
||||
content_type = content_types.get(ext, 'application/octet-stream')
|
||||
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
file_data = f.read()
|
||||
|
||||
# Resize if width is specified and it's an image
|
||||
if width and PIL_AVAILABLE and content_type.startswith('image/'):
|
||||
try:
|
||||
file_data = resize_image(file_data, width)
|
||||
# After resize, it's always JPEG (unless PNG with alpha)
|
||||
if ext != '.png':
|
||||
content_type = 'image/jpeg'
|
||||
except Exception as e:
|
||||
print(f"[FileServer] Resize error: {e}")
|
||||
# Fall through to serve original
|
||||
|
||||
return file_data, content_type, 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FileServer] Error reading {file_path}: {e}")
|
||||
return None, 'text/plain', 500
|
||||
|
||||
|
||||
def get_file_url(cid: str, base_url: str, width: Optional[int] = None) -> str:
|
||||
"""
|
||||
Generate a file URL for a CID.
|
||||
|
||||
Args:
|
||||
cid: The CID of the file
|
||||
base_url: Base URL of the server
|
||||
width: Optional width for resizing (images only)
|
||||
|
||||
Returns:
|
||||
Full URL to the file
|
||||
"""
|
||||
if not cid:
|
||||
return ""
|
||||
|
||||
if width:
|
||||
return f"{base_url}/file/{cid}/w{width}"
|
||||
return f"{base_url}/file/{cid}"
|
||||
Reference in New Issue
Block a user