175 lines
4.8 KiB
Python
175 lines
4.8 KiB
Python
"""
|
|
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}"
|