initial commit

This commit is contained in:
Pierre
2026-01-11 17:27:01 +01:00
commit b9654c5b8d
20 changed files with 6635 additions and 0 deletions
+174
View File
@@ -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}"