Conventions de code¶
Cette page documente les conventions, règles et contraintes qui s'appliquent à l'ensemble du codebase Stream Fusion. Tout contributeur doit les connaître avant de modifier le code.
I/O asynchrone obligatoire¶
Toute la chaîne de traitement des requêtes est asynchrone. Aucune opération réseau ou base de données synchrone n'est tolérée dans le chemin de requête.
| Technologie | Client | Usage |
|---|---|---|
| HTTP sortant | aiohttp.ClientSession |
API debrid, indexeurs, TMDB, IMDB |
| PostgreSQL | asyncpg via SQLAlchemy 2.0 async |
Toutes les requêtes DB |
| Redis | redis-py (ConnectionPool) |
Cache, verrous, résultats |
# ✅ Correct : tout est async
async with app.state.http_session.get(url) as resp:
data = await resp.json()
async with app.state.db_session_factory() as session:
result = await session.execute(select(Model))
# ❌ Incorrect : bloquant — bloque le worker Gunicorn
import requests
resp = requests.get(url)
import psycopg2
conn = psycopg2.connect(...)
Pas d'appels synchrones dans le chemin de requête
Un appel HTTP synchrone (requests, urllib) ou une connexion DB synchrone (psycopg2) bloque le worker Gunicorn entier pendant toute la durée de l'opération. Cela réduit la capacité de traitement et peut causer des timeouts en cascade.
Logging (Loguru)¶
Le système de logging est configuré dans stream_fusion/logging_config.py :
from stream_fusion.logging_config import logger
logger.info("Message informatif")
logger.debug("Message de debug")
logger.error("Erreur critique")
Caractéristiques¶
| Aspect | Détail |
|---|---|
| Bibliothèque | Loguru (pas logging standard) |
| Censure | JWT, tokens API, URLs de téléchargement automatiquement caviardés |
| Niveau | LOG_LEVEL (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL) — chaud via admin |
| Console | Sortie standard avec couleurs, horodatage, fonction:ligne |
| Fichier | Un fichier par worker PID : stream-fusion.w{pid}.log |
| Rotation | Quotidienne, rétention 7 jours, compression ZIP |
| Interception | Capture les logs de la bibliothèque standard (uvicorn, httpx) |
Changement de niveau à chaud
Le niveau de log peut être modifié via le panneau d'administration (/admin/settings → Niveau de verbosité). Le changement prend effet immédiatement dans le worker courant sans redémarrage, car SettingsService.set("log_level", ...) appelle automatiquement configure_logging().
Patterns de censure¶
patterns = [
r"/ey[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]*/", # JWT
r"/db/(?:duckdb|meilisearch)/dump/download/([a-f0-9]{64})", # Download tokens
]
Les messages sont censurés avant d'atteindre les handlers — le filtrage est implémenté via SecretFilter.
Assignations Redis DB¶
Stream Fusion utilise deux bases de données Redis distinctes avec des rôles bien définis :
| DB Redis | Rôle | Contenu |
|---|---|---|
| DB 5 | Cache SF | Résultats de recherche, disponibilité debrid, métadonnées TMDB cache, verrous de refresh, verrous de préchargement, tokens de téléchargement de dumps |
| DB 6 | Broker Taskiq | Files d'attente de tâches, résultats de tâches, sources de planification cron (sfr:sched:*) |
Ne pas inverser
DB5 et DB6 ont des politiques d'expiration et des schémas de clés complètement différents. Ne jamais stocker de données applicatives dans DB6 — elles seraient supprimées par les opérations de maintenance Redis. Ne jamais configurer le broker Taskiq sur DB5 — cela provoquerait la consommation de résultats de cache comme des tâches.
| Variable | Valeur | DB |
|---|---|---|
REDIS_DB |
5 |
DB cache applicatif |
TASKIQ_REDIS_DB |
6 |
DB broker Taskiq |
Compatibilité PgBouncer¶
La connexion PostgreSQL est configurée pour être compatible avec PgBouncer en mode transaction :
engine = create_async_engine(
str(settings.pg_url),
pool_size=settings.pg_pool_size,
max_overflow=settings.pg_max_overflow,
pool_pre_ping=True, # Valide la connexion avant usage
pool_recycle=1800, # Recycle après 30 min (avant le timeout PgBouncer)
connect_args={
"statement_cache_size": 0, # Requis pour PgBouncer (transaction mode)
},
)
| Paramètre | Valeur | Raison |
|---|---|---|
pool_pre_ping |
True |
Détecte les connexions mortes (PgBouncer peut en fermer silencieusement) |
pool_recycle |
1800 (30 min) |
Recycle avant le server_idle_timeout de PgBouncer |
statement_cache_size |
0 |
Désactive le cache de statements préparés — incompatible avec PgBouncer |
Imports conditionnels¶
constants.py — patterns lourds¶
Les patterns de regex géants (FR_RELEASE_GROUPS, FRENCH_PATTERNS) sont dans stream_fusion/constants.py et ne doivent jamais être importés dans des chemins froids :
# ✅ Correct : import tardif, uniquement quand nécessaire
def detect_french_release(title: str) -> bool:
from stream_fusion.constants import FRENCH_PATTERNS
return bool(FRENCH_PATTERNS.search(title))
# ❌ Incorrect : import au niveau module — chargé à chaque démarrage
from stream_fusion.constants import FRENCH_PATTERNS
Ces patterns peuvent faire plusieurs dizaines de kilooctets et leur compilation est coûteuse.
Normalisation centralisée des titres¶
La fonction normalize_title() dans utils/metadata/imdb/title_normalizer.py est le point d'entrée unique pour la normalisation des titres. Elle est utilisée par :
- DuckDB (insert) — les titres insérés dans la base IMDB sont normalisés
- Meilisearch (enrichissement) — les titres enrichis sont normalisés avec la même fonction
- Recherche live — les titres recherchés sont normalisés avant matching
Garantie de parité
Cette centralisation garantit que le matching fonctionne de manière cohérente entre les bases DuckDB (interne) et Meilisearch (externe). Un titre normalisé de la même façon produira le même hash de recherche, quelle que soit la source.
Convention de nommage des indexeurs¶
Chaque indexeur privé suit un pattern 3 fichiers dans indexers/private/{nom}/ :
| Fichier | Rôle |
|---|---|
{nom}_api.py |
Client HTTP bas niveau — retourne les objets bruts de l'API |
{nom}_result.py |
Modèle de résultat — conversion raw → dataclass → TorrentItem |
{nom}_service.py |
Orchestrateur — logique métier, cache, persistance PG |
Les indexeurs publics (indexers/public/) sont éphémères — leurs résultats ne sont jamais stockés dans PostgreSQL.
Exemples :
indexers/private/c411/c411_api.py
indexers/private/c411/c411_result.py
indexers/private/c411/c411_service.py
indexers/public/zilean/zilean_api.py
indexers/public/zilean/zilean_service.py
Checklist pour ajouter un indexeur
L'ajout d'un indexeur privé nécessite de toucher environ 14 fichiers. Voir le guide complet dans Support > Ajouter un indexeur privé.
Stratégie de branches¶
| Opération | Branche |
|---|---|
| Base de développement | develop |
| Base de production | master |
| Nouvelle fonctionnalité | Brancher depuis develop |
| Pull request | Cibler develop |
Séquence de démarrage (lifespan.py)¶
L'ordre d'initialisation dans web/lifespan.py est critique — chaque étape dépend de la précédente :
sequenceDiagram
participant App as FastAPI
participant DB as PostgreSQL
participant MW as Middleware
participant HTTP as aiohttp
participant Redis as Redis
participant Svc as SettingsService
participant TM as Title Matching
participant Meili as Meilisearch
participant Duck as DuckDB
participant Peer as PeerRegistry
participant TQ as Taskiq
App->>DB: 1. create_async_engine() + session_factory
App->>MW: 2. build_middleware_stack()
App->>HTTP: 3. aiohttp sessions (général + debrid)
App->>Redis: 4. ConnectionPool
App->>Svc: 5. SettingsService — seed_defaults() + apply_overrides
App->>TM: 6. initialize_title_matching()
App->>Meili: 7. init_meilisearch()
App->>Duck: 8. DuckDB readiness check + cache
App->>Peer: 9. PeerRegistry.refresh()
App->>TQ: 10. broker.startup()
Ne pas réorganiser
seed_defaults() doit s'exécuter avant apply_overrides_to_singleton() pour capturer les valeurs env par défaut. Les sessions aiohttp doivent être créées avant l'initialisation de Meilisearch car l'enrichissement IMDB utilise ces sessions.
| Étape | Composant | Dépend de |
|---|---|---|
| 1 | Connexion PostgreSQL | Rien |
| 2 | Middleware FastAPI | Étape 1 |
| 3 | Sessions aiohttp | Paramètres env (proxy) |
| 4 | Pool Redis | Configuration réseau |
| 5 | SettingsService | Steps 1, 4 |
| 6 | Title matching | Steps 1, 4, 5 |
| 7 | Meilisearch | Steps 3, 5 |
| 8 | DuckDB readiness | Rien (vérification indépendante) |
| 9 | PeerRegistry | Step 1 |
| 10 | Broker Taskiq | Steps 5 |
Factory d'application¶
L'application FastAPI est construite par une factory dans web/application.py :
def get_app() -> FastAPI:
app = FastAPI(
title="StreamFusion",
lifespan=lifespan_setup,
docs_url=None, # Swagger masqué (security_hide_docs)
redoc_url=None,
openapi_url="/api/openapi.json",
)
app.add_middleware(CORSMiddleware, ...)
app.add_middleware(SessionMiddleware, secret_key=settings.session_key)
app.include_router(router=root_router) # Stremio: /configure, /manifest, /catalog, /stream
app.include_router(router=register_router) # /register (inscription publique)
app.include_router(router=stream_router) # /playback (proxy de stream)
app.include_router(router=api_router, prefix="/api") # API interne
app.include_router(router=admin_router, prefix="/admin") # Admin dashboard
app.mount("/static", StaticFiles(...), name="static")
return app