wastream-mirror/wastream/utils/logger.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

225 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
import sys
import logging
from collections import deque
from contextvars import ContextVar
from threading import Lock
from typing import List, Dict, Optional
from loguru import logger
# ===========================
# Log Contexts & Constants
# ===========================
PREFIX_PATTERN = re.compile(r'^\[([^\]]+)\]')
user_id_var: ContextVar[Optional[str]] = ContextVar("user_id", default=None)
LOG_LEVEL = "INFO"
MAX_LOG_BUFFER = 5000
CONTEXTS = {
"ADDON": {"color": "green", "icon": "🚀", "hex": "#22c55e"},
"API": {"color": "cyan", "icon": "🔗", "hex": "#06b6d4"},
"STREAM": {"color": "yellow", "icon": "🎬", "hex": "#eab308"},
"SCRAPER": {"color": "blue", "icon": "🌐", "hex": "#3b82f6"},
"DEBRID": {"color": "magenta", "icon": "☁️", "hex": "#a855f7"},
"METADATA": {"color": "white", "icon": "🎭", "hex": "#f5f5f5"},
"CACHE": {"color": "white", "icon": "💾", "hex": "#f5f5f5"},
"DATABASE": {"color": "yellow", "icon": "🗄️", "hex": "#eab308"},
"REMOTE": {"color": "red", "icon": "🔄", "hex": "#ef4444"},
}
LEVEL_ICONS = {
"DEBUG": "🔍",
"INFO": " ",
"WARNING": "⚠️",
"ERROR": "",
}
LEVEL_COLORS = {
"DEBUG": "#71717a",
"INFO": "#3b82f6",
"WARNING": "#f59e0b",
"ERROR": "#ef4444",
}
# ===========================
# Log Buffer
# ===========================
class LogBuffer:
def __init__(self, max_size: int = MAX_LOG_BUFFER):
self._buffer: deque = deque(maxlen=max_size)
self._lock = Lock()
def add(self, log_entry: Dict):
with self._lock:
self._buffer.append(log_entry)
def get_logs(self, since: float = 0, context: Optional[str] = None, level: Optional[str] = None, prefix: Optional[str] = None, search: Optional[str] = None, user_id: Optional[str] = None) -> List[Dict]:
with self._lock:
logs = list(self._buffer)
if since > 0:
logs = [log for log in logs if log["timestamp"] > since]
if context:
logs = [log for log in logs if log["context"] == context]
if level:
level_priority = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3}
min_priority = level_priority.get(level.upper(), 0)
logs = [log for log in logs if level_priority.get(log["level"], 0) >= min_priority]
if prefix:
if prefix == "__none__":
logs = [log for log in logs if not PREFIX_PATTERN.match(log["message"])]
else:
logs = [log for log in logs if log["message"].startswith(f"[{prefix}]")]
if user_id:
user_id_lower = user_id.lower()
logs = [log for log in logs if log.get("user_id") and user_id_lower in log["user_id"].lower()]
if search:
search_lower = search.lower()
logs = [
log for log in logs
if search_lower in log["message"].lower()
or (log.get("user_id") and search_lower in log["user_id"].lower())
]
return logs
def get_available_prefixes(self) -> List[str]:
with self._lock:
logs = list(self._buffer)
prefixes = set()
for log in logs:
match = PREFIX_PATTERN.match(log["message"])
if match:
prefixes.add(match.group(1))
return sorted(prefixes)
def clear(self):
with self._lock:
self._buffer.clear()
log_buffer = LogBuffer()
# ===========================
# Log Sinks & Formatting
# ===========================
def log_sink(message):
record = message.record
context = record["extra"].get("context", "ADDON")
context_data = CONTEXTS.get(context, {"icon": "📦", "hex": "#71717a"})
level_name = record["level"].name
user_id = user_id_var.get()
log_entry = {
"timestamp": record["time"].timestamp(),
"time": record["time"].strftime("%Y-%m-%d %H:%M:%S"),
"level": level_name,
"level_icon": LEVEL_ICONS.get(level_name, ""),
"level_color": LEVEL_COLORS.get(level_name, "#71717a"),
"context": context,
"context_icon": context_data["icon"],
"context_color": context_data["hex"],
"message": record["message"],
"user_id": user_id,
}
log_buffer.add(log_entry)
def format_log(record):
context = record["extra"].get("context", "ADDON")
context_data = CONTEXTS.get(context, {"color": "white", "icon": "📦"})
context_color = context_data["color"]
context_icon = context_data["icon"]
level_icon = LEVEL_ICONS.get(record["level"].name, "")
user_id = user_id_var.get()
user_segment = f"<yellow>👤 {user_id[:8]}</yellow> | " if user_id else ""
return (
"<white>{time:YYYY-MM-DD}</white> "
"<magenta>{time:HH:mm:ss}</magenta> | "
f"<level>{level_icon} {{level: <8}}</level> | "
f"<{context_color}>{context_icon} {{extra[context]: <10}}</{context_color}> | "
f"{user_segment}"
"<level>{message}</level>\n"
)
# ===========================
# Logger Setup
# ===========================
def setup_logger(level: str = "INFO"):
global LOG_LEVEL
LOG_LEVEL = level
logger.remove()
logger.add(
sys.stderr,
level=LOG_LEVEL,
format=format_log,
colorize=True,
backtrace=True,
diagnose=True,
)
logger.add(
log_sink,
level="DEBUG",
format="{message}",
)
# ===========================
# Log Accessors
# ===========================
def get_logger(context: str):
return logger.bind(context=context)
def get_logs(since: float = 0, context: Optional[str] = None, level: Optional[str] = None, prefix: Optional[str] = None, search: Optional[str] = None, user_id: Optional[str] = None) -> List[Dict]:
return log_buffer.get_logs(since, context, level, prefix, search, user_id)
def clear_logs():
log_buffer.clear()
def get_available_contexts() -> List[str]:
return list(CONTEXTS.keys())
def get_available_prefixes() -> List[str]:
return log_buffer.get_available_prefixes()
# ===========================
# Logger Singletons
# ===========================
addon_logger = get_logger("ADDON")
api_logger = get_logger("API")
stream_logger = get_logger("STREAM")
scraper_logger = get_logger("SCRAPER")
debrid_logger = get_logger("DEBRID")
metadata_logger = get_logger("METADATA")
cache_logger = get_logger("CACHE")
database_logger = get_logger("DATABASE")
remote_logger = get_logger("REMOTE")
logging.getLogger("uvicorn.access").disabled = True
logging.getLogger("uvicorn.error").setLevel(logging.CRITICAL)
logging.getLogger("fastapi").setLevel(logging.CRITICAL)