wastream-mirror/wastream/utils/validators.py
10ho 1cab0df19f v3.7.0: admin dashboard editing, DB & config backup, resilient playback
FIX:
- WASource: series <-> movie conversion now works both ways (was movie -> series only)
- Turbobit: canonicalize mirror domains (trbt.cc / turbobit.cc -> turbobit.net)

ADD:
- WASource: editable IMDB ID from the dashboard
- Admin: export / import of the database data (backup)
- Admin: all settings editable from the dashboard (values set in the .env stay read-only)
- Admin: content cache browser (Cache tab) to view cached content / links
- Config: resilient playback (fallback + racing), configurable in the user config
- Config: export / import of the user config on the /configure page (credentials excluded)
- Pastebin: multi-host support via known-domain detection (now also supports 1fichier links, etc.)
- Pastebin: recursive automatic link discovery (index -> content)
2026-07-17 21:44:29 +02:00

166 lines
5.2 KiB
Python

import binascii
import json
from base64 import b64decode
from typing import Optional, Dict, List
from pydantic import BaseModel, field_validator, ConfigDict
from wastream.config.settings import settings
from wastream.utils.logger import api_logger
from wastream.utils.quality import AVAILABLE_RESOLUTIONS
# ===========================
# Pydantic Config Models
# ===========================
class DebridServiceEntry(BaseModel):
model_config = ConfigDict(extra="allow")
service: str
api_key: str
hosts: List[str] = []
sources: List[str] = []
enable_nzb: bool = False
enable_full_season: bool = False
@field_validator("service")
@classmethod
def validate_service(cls, v):
if v not in settings.DEBRID_SERVICES:
raise ValueError(f"Invalid debrid service: {v}")
return v
@field_validator("api_key")
@classmethod
def validate_api_key(cls, v):
if not v:
raise ValueError("api_key must not be empty")
return v
class UserConfig(BaseModel):
model_config = ConfigDict(extra="allow")
tmdb_api_token: str
debrid_services: List[DebridServiceEntry]
excluded_keywords: List[str] = []
languages: List[str] = []
resolutions: List[str] = AVAILABLE_RESOLUTIONS
max_results_per_resolution: int = 0
max_size_gb: float = 0.0
recheck_hoster_status: bool = False
@field_validator("tmdb_api_token")
@classmethod
def validate_tmdb_token(cls, v):
if not v:
raise ValueError("tmdb_api_token must not be empty")
return v
@field_validator("debrid_services")
@classmethod
def validate_debrid_services(cls, v):
if not v:
raise ValueError("At least one debrid service is required")
return v
@field_validator("max_results_per_resolution")
@classmethod
def validate_max_results(cls, v):
if v < 0:
raise ValueError("max_results_per_resolution must be >= 0")
return v
@field_validator("max_size_gb", mode="before")
@classmethod
def coerce_max_size(cls, v):
if isinstance(v, (int, float)):
if v < 0:
raise ValueError("max_size_gb must be >= 0")
return float(v)
return v
# ===========================
# Configuration Validation
# ===========================
def validate_config(config_base64: Optional[str]) -> Optional[Dict]:
if not config_base64:
api_logger.debug("Empty config provided")
return None
try:
api_logger.debug("Validating configuration")
decoded_bytes = b64decode(config_base64, validate=True)
decoded_str = decoded_bytes.decode('utf-8')
config_dict = json.loads(decoded_str)
if not isinstance(config_dict, dict):
api_logger.debug("Config is not a dict")
return None
# Legacy single-service format → convert to multi-service
if "debrid_services" not in config_dict:
if "debrid_service" in config_dict and "debrid_api_key" in config_dict:
if not config_dict["debrid_service"] or not config_dict["debrid_api_key"]:
api_logger.debug("Missing or empty debrid_service/debrid_api_key")
return None
config_dict["debrid_services"] = [{
"service": config_dict["debrid_service"],
"api_key": config_dict["debrid_api_key"]
}]
else:
api_logger.debug("Missing debrid configuration")
return None
validated = UserConfig.model_validate(config_dict)
result = validated.model_dump()
# Merge back any extra fields from the original config (sort_order, early_stop, nzbdav_url, etc.)
for key, value in config_dict.items():
if key not in result:
result[key] = value
api_logger.debug("Configuration validated successfully")
return result
except (binascii.Error, UnicodeDecodeError, json.JSONDecodeError) as e:
api_logger.debug(f"Config validation failed: {type(e).__name__}")
return None
except Exception as e:
api_logger.debug(f"Config validation failed: {type(e).__name__}")
return None
# ===========================
# Media Info Extraction
# ===========================
def extract_media_info(content_id: str, content_type: str) -> Dict[str, Optional[str]]:
content_id_formatted = content_id.replace(".json", "")
if content_id_formatted.startswith("kitsu:"):
parts = content_id_formatted.split(":")
return {
"kitsu_id": parts[1] if len(parts) > 1 else "",
"episode": parts[2] if len(parts) > 2 else None,
"season": "1",
"imdb_id": None
}
if content_type == "series" and ":" in content_id_formatted:
parts = content_id_formatted.split(":")
return {
"imdb_id": parts[0],
"season": parts[1] if len(parts) > 1 else "1",
"episode": parts[2] if len(parts) > 2 else "1",
"kitsu_id": None
}
return {
"imdb_id": content_id_formatted,
"season": None,
"episode": None,
"kitsu_id": None
}