Quesaco ?
Infra de MainmainFlix¶
Comment ça marche ?¶
Tout ce système repose sur quelques solutions Open Source: Plex, Radarr, Sonarr, un indexer et ton client torrent préféré.
Pour une automatisation complète nous pouvons rajouter des briques (solutions) par-ci par-là afin de répondre à certains besoins.
Documentation importante: https://trash-guides.info
Les principales solutions¶
Plex est une solution Open Source qui permet de créer des librairies et de pouvoir consommer leur contenu en VOD. Il est possible de donner accès aux différentes librairies à d'autres utilisateurs.
Radarr est une solution Open Source qui permet de manager les demandes de film dans un ou plusieurs path.
Il va être capable de récupérer les metadatas du film mais aussi de pouvoir se connecter via API à un client Torrent pour télécharger directement le média. En rajoutant un indexer, il va être capable d'aller pull sur ce dernier le film qu'il aimerait télécharger (vous voyez déjà venir l'automatisation) et l'envoyer au client Torrent.
Sonarr est exactement la même solution que Radarr mais pour les séries (même features, l'un est un fork de l'autre). Client Torrent (ici qBitTorrent) est un downloader de torrent, celui-ci a pas mal de paramètres et possède une api, ce qui nous intéresse fortement.
Un indexer (Prowlarr) qui va permettre l'aller chercher sur différent site de torrent les medias que tu cherches.
Tout connecter¶
Directories¶
Le but va être de créer une structure de répertoire permettant d'organiser le tout correctement:
- le client Torrent doit avoir un path dédié pour le téléchargement des films, pareil pour les séries
- Radarr va avoir un path dédié pour les films
- Sonarr va avoir un path dédié pour les séries
- Plex doit avoir accès aux paths de Radarr et Sonarr pour consommer ces librairies.
Globalement ça ressemble à ça:
data
├── torrents
│ ├── movies
│ └── tv
├── usenet
│ ├── incomplete
│ └── complete
│ ├── movies
│ └── tv
└── media
├── movies
└── tv
Connexions¶
- qBitTorrent va être utilisé par Radarr et Sonarr pour télécharger
- Prowlarr va être utilisé par Radarr et Sonarr pour chercher les fichiers torrent
- Plex va être utilisé par le consommateur VOD
Full automation¶
Solutions¶
Pour avoir un gestion complètement automatiser nous pouvons rajouter plusieurs solutions:
- Overseerr qui permet d'avoir un super portail pour les utilisateurs pour leur demande de films
- Kuma qui va permettre de faire du monitoring de l'infrastructure et de l'alerting en cas de pépins
- Slack/Discord avec un channel dédié pour l'alerting
- Tautulli pour analyser ce qui est consommer sur le Plex par les utilisateurs, ça permet d'avoir des metrics supplémentaires
- FlareSolverr qui permet de passer les capcha Cloudflare
Connexion¶
- Tautulli va consommer Plex
- FlareSolverr va être consommer par Prowlarr
- Kuma va poller les différents endpoints pour surveiller leur status et utiliser Slack pour envoyer de l'alerting
- Overseerr va être connecter à Plex ainsi que Radarr et Sonarr
- Overseerr Focus, frontend of our system
- Overseerr va être la plateform où les utilisateurs vont faire leurs demandes.
Lors qu'une demande est faite, il va envoyer celle ci directement à Radarr ou Sonarr (au choix par rapport au média demandé) et ces derniers vont s'occuper du reste avec les autres composants.
Schéma:¶
flowchart TB
subgraph Users["👥 Utilisateurs"]
User[Utilisateur]
end
subgraph Frontend["🎬 Interface"]
Plex[("PlexStreaming VOD")]
Overseerr["OverseerrDemandes"]
end
subgraph Management["📥 Gestion des médias"]
Radarr["RadarrFilms"]
Sonarr["SonarrSéries"]
Prowlarr["ProwlarrIndexer"]
end
subgraph Download["⬇️ Téléchargement"]
qBit["qBitTorrentClient Torrent"]
FlareSolverr["FlareSolverrBypass Captcha"]
end
subgraph Storage["💾 Stockage"]
TorrentDir["/downloadsTéléchargements"]
MoviesDir["/moviesFilms"]
SeriesDir["/seriesSéries"]
end
subgraph Monitoring["📊 Monitoring"]
Tautulli["TautulliAnalytics Plex"]
Kuma["Uptime KumaMonitoring"]
Alerts["Slack/DiscordAlertes"]
end
%% User flows
User -->|"Regarde"| Plex
User -->|"Demande"| Overseerr
%% Overseerr connections
Overseerr -->|"Film demandé"| Radarr
Overseerr -->|"Série demandée"| Sonarr
Overseerr -.->|"Sync utilisateurs"| Plex
%% Arr stack
Radarr -->|"Recherche"| Prowlarr
Sonarr -->|"Recherche"| Prowlarr
Prowlarr -->|"Bypass CF"| FlareSolverr
Prowlarr -->|"Télécharge"| qBit
%% Download flow
qBit -->|"Sauvegarde"| TorrentDir
Radarr -->|"Importe"| MoviesDir
Sonarr -->|"Importe"| SeriesDir
%% Plex access
Plex -.->|"Lit"| MoviesDir
Plex -.->|"Lit"| SeriesDir
%% Monitoring
Tautulli -->|"Analyse"| Plex
Kuma -->|"Poll"| Plex
Kuma -->|"Poll"| Overseerr
Kuma -->|"Poll"| Radarr
Kuma -->|"Poll"| Sonarr
Kuma -->|"Alerte"| Alerts
Infra de l'IA de ce site¶
Pour générer des résumés automatiques, j'utilise les WorkerAI de Cloudflare.
Securité¶
Il fallait déjà sécuriser les appels, pour ça il y a un worker dédié par lequel toutes les requêtes passent.
Il permet de vérifier les tokens d'authentification, rajouter un ratelimit, etc ...
Un petit bout de code:
"""Security gateway worker - handles auth and forwards to API worker."""
import logging
import json
import js
from workers import WorkerEntrypoint, Response
ALLOWED_ORIGINS = [
"",
"",
"",
"",
]
CORS_HEADERS = {
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, API_KEY_TOKEN",
}
# Rate limit config
RATE_LIMIT_MAX = int
RATE_LIMIT_WINDOW = int
def get_timestamp():
"""Retourne le timestamp actuel en secondes."""
return int(js.Date.now() / 1000)
def error_response(detail: str, status: int = 400, origin: str = None):
headers = {"Content-Type": "application/json"}
if origin and origin in ALLOWED_ORIGINS:
headers["Access-Control-Allow-Origin"] = origin
return Response(
json.dumps({"detail": detail}),
status=status,
headers=headers
)
def add_cors_headers(response: Response, origin: str) -> Response:
headers = {**dict(response.headers), **CORS_HEADERS}
if origin in ALLOWED_ORIGINS:
headers["Access-Control-Allow-Origin"] = origin
return Response(
response.body,
status=response.status,
headers=headers
)
class Default(WorkerEntrypoint):
"""Gateway Worker - Auth and forward."""
async def _check_rate_limit(self, ip: str) -> bool:
"""Retourne True si rate limit dépassé."""
key = f"ratelimit:{ip}"
now = get_timestamp()
window_start = now - RATE_LIMIT_WINDOW
try:
data = await self.env.KV.get(key)
if data:
record = json.loads(data)
requests = [ts for ts in record["requests"] if ts > window_start]
else:
requests = []
if len(requests) >= RATE_LIMIT_MAX:
return True
requests.append(now)
await self.env.KV.put(
key,
json.dumps({"requests": requests}),
expiration_ttl=RATE_LIMIT_WINDOW
)
return False
except Exception as e:
logging.error(f"Rate limit error: {e}")
return False
async def fetch(self, request):
url = request.url
method = request.method
path = "/" + url.split("/", 3)[-1]
origin = request.headers.get("Origin")
client_ip = request.headers.get("CF-Connecting-IP", "unknown")
# Handle CORS preflight
if method == "OPTIONS":
headers = {**CORS_HEADERS}
if origin in ALLOWED_ORIGINS:
headers["Access-Control-Allow-Origin"] = origin
return Response("", headers=headers)
# Route PUBLIQUE avec rate limit
if path.startswith("/le/path/qui/va/bien"):
if origin not in ALLOWED_ORIGINS:
return error_response("Forbidden", 403, origin)
if await self._check_rate_limit(client_ip):
return error_response("Rate limit exceeded. Try again later.", 429, origin)
response = await self.env.AI_WORKER.fetch(request)
return add_cors_headers(response, origin)
# Health endpoint
if path.startswith("/le/path/qui/va/bien"):
auth_error = await self._verify_headers(
request=request,
header_name="header spécifique pour le health check",
secret=self.env.LE_TOKEN_QUI_VA_BIEN,
)
if auth_error:
return auth_error
# Other endpoints need token
else:
auth_error = await self._verify_headers(
request=request,
header_name="X-API-Token",
secret=self.env.API_KEY_TOKEN,
)
if auth_error:
return auth_error
# Forward to AI worker
if path.startswith("/le/path/qui/va/bien"):
response = await self.env.A_FIRST_WORKER.fetch(request)
return add_cors_headers(response, origin)
# Forward to API worker
response = await self.env.ANOTHER_WORKER.fetch(request)
return add_cors_headers(response, origin)
async def _verify_headers(
self,
request,
header_name: str,
secret: any,
) -> Response | None:
try:
token = request.headers.get(header_name)
if not token:
return error_response("Token not valid", 403)
secret_binding = secret
if hasattr(secret_binding, 'get') and callable(secret_binding.get):
secret_value = await secret_binding.get()
else:
secret_value = secret_binding
if token != secret_value:
return error_response("Token not valid", 403)
return None
except Exception as e:
logging.error(f"Auth error: {e}")
return error_response("Authentication error", 500)
IA et Summarize¶
J'ai créer une API à base de plusieurs Worker qui font appel aux models locaux chez Cloudflare.
Voilà le petit obut de code:
from workers import WorkerEntrypoint, Response
import json
import js
class Default(WorkerEntrypoint):
async def fetch(self, request):
body = await request.json()
prompt = body.get("prompt")
if prompt is None:
return Response(
json.dumps({"error": "Bad request"}),
status=400,
headers={"Content-Type": "application/json"},
)
try:
result = await self.env.AI.run(
"@cf/le_bon/model",
js.JSON.parse(json.dumps({
"messages": [
{
"role": "user",
"content": "Le petit prompt qui va bien avec la page qu'on veut summarize."
}
],
"max_tokens": int
})),
)
# Convertir via JSON.stringify
result_str = js.JSON.stringify(result)
result_dict = json.loads(result_str)
summary = result_dict.get('response', str(result_dict))
return Response(
json.dumps({"result": {"summary": summary}}),
status=200,
headers={"Content-Type": "application/json"},
)
except Exception as e:
return Response(
json.dumps({"error": str(e)}),
status=500,
headers={"Content-Type": "application/json"},
)