mirror of
https://github.com/vatax3/ranger.git
synced 2026-08-11 19:15:43 +00:00
Addon Stremio complet, films/séries/anime, contenu FR et international : - Multi-débrideurs avec priorité (AllDebrid, Real-Debrid, TorBox, DebridLink) - StremThru intégré (contournement blocage IP datacenter) - Compat AIOStreams (tags [XX+], statut cache) - TMDB + fallback Cinemeta, détection anime, numérotation absolue - Trackers publics (YGG, TPB, EZTV, Nyaa) + privés (C411, Torr9, Tr4ker, NekoBT, ABN, UNIT3D) + Torznab générique (Jackett/Prowlarr) - Filtres taille/résolution/codec/langue, tri multi-critères, déduplication - Cache SQLite (dispo débrideur + recherches + métadonnées) - Page /configure interactive, image Docker + docker-compose Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
165 lines
5.6 KiB
Python
165 lines
5.6 KiB
Python
"""
|
||
Métadonnées du média demandé : titre FR + titre original + année + détection
|
||
anime + numérotation absolue des épisodes.
|
||
|
||
Source principale : TMDB (clé utilisateur). Fallback : Cinemeta (sans clé,
|
||
titres anglais uniquement).
|
||
Résultats mis en cache SQLite (TTL 7 jours).
|
||
"""
|
||
|
||
import logging
|
||
|
||
import aiohttp
|
||
|
||
from core import cache
|
||
|
||
TMDB_BASE = "https://api.themoviedb.org/3"
|
||
CINEMETA_BASE = "https://v3-cinemeta.strem.io/meta"
|
||
|
||
|
||
async def get_media_info(imdb_id, stream_type, tmdb_key):
|
||
"""
|
||
Retourne un dict :
|
||
{tmdb_id, title, original_title, year, is_anime, seasons: [{season_number, episode_count}]}
|
||
ou None si introuvable.
|
||
"""
|
||
cache_key = f"media:{stream_type}:{imdb_id}:{'tmdb' if tmdb_key else 'cinemeta'}"
|
||
cached = cache.get_meta(cache_key)
|
||
if cached:
|
||
return cached
|
||
|
||
info = None
|
||
if tmdb_key:
|
||
info = await _from_tmdb(imdb_id, stream_type, tmdb_key)
|
||
if not info:
|
||
info = await _from_cinemeta(imdb_id, stream_type)
|
||
|
||
if info:
|
||
cache.set_meta(cache_key, info)
|
||
return info
|
||
|
||
|
||
async def _from_tmdb(imdb_id, stream_type, tmdb_key):
|
||
kind = "movie" if stream_type == "movie" else "tv"
|
||
try:
|
||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||
# 1. IMDB -> TMDB
|
||
async with session.get(
|
||
f"{TMDB_BASE}/find/{imdb_id}",
|
||
params={"api_key": tmdb_key, "external_source": "imdb_id"},
|
||
timeout=aiohttp.ClientTimeout(total=10),
|
||
) as resp:
|
||
if resp.status != 200:
|
||
logging.warning(f"TMDB find HTTP {resp.status}")
|
||
return None
|
||
data = await resp.json()
|
||
results = data.get("movie_results" if kind == "movie" else "tv_results", [])
|
||
if not results:
|
||
return None
|
||
tmdb_id = results[0]["id"]
|
||
|
||
# 2. Détails complets en FR (titre FR + saisons + genres)
|
||
async with session.get(
|
||
f"{TMDB_BASE}/{kind}/{tmdb_id}",
|
||
params={"api_key": tmdb_key, "language": "fr-FR"},
|
||
timeout=aiohttp.ClientTimeout(total=10),
|
||
) as resp:
|
||
if resp.status != 200:
|
||
return None
|
||
details = await resp.json()
|
||
except Exception as e:
|
||
logging.error(f"TMDB error: {e}")
|
||
return None
|
||
|
||
title = details.get("title") or details.get("name") or ""
|
||
original_title = details.get("original_title") or details.get("original_name") or ""
|
||
date = details.get("release_date") or details.get("first_air_date") or ""
|
||
year = date.split("-")[0] if date else ""
|
||
|
||
# Détection anime (logique héritée de Frenchio)
|
||
orig_lang = details.get("original_language", "")
|
||
origin_country = details.get("origin_country", [])
|
||
genre_ids = [g.get("id") for g in details.get("genres", [])]
|
||
is_anime = False
|
||
if orig_lang in ("ja", "ko", "zh") or "JP" in origin_country:
|
||
if 16 in genre_ids or orig_lang == "ja":
|
||
is_anime = True
|
||
if "anime" in (details.get("overview") or "").lower() and 16 in genre_ids:
|
||
is_anime = True
|
||
|
||
seasons = [
|
||
{"season_number": s.get("season_number"), "episode_count": s.get("episode_count")}
|
||
for s in details.get("seasons", [])
|
||
if s.get("season_number") is not None
|
||
]
|
||
|
||
return {
|
||
"tmdb_id": tmdb_id,
|
||
"title": title,
|
||
"original_title": original_title,
|
||
"year": year,
|
||
"is_anime": is_anime,
|
||
"seasons": seasons,
|
||
}
|
||
|
||
|
||
async def _from_cinemeta(imdb_id, stream_type):
|
||
kind = "movie" if stream_type == "movie" else "series"
|
||
try:
|
||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||
async with session.get(
|
||
f"{CINEMETA_BASE}/{kind}/{imdb_id}.json",
|
||
timeout=aiohttp.ClientTimeout(total=10),
|
||
) as resp:
|
||
if resp.status != 200:
|
||
return None
|
||
data = await resp.json()
|
||
except Exception as e:
|
||
logging.error(f"Cinemeta error: {e}")
|
||
return None
|
||
|
||
meta = data.get("meta") or {}
|
||
if not meta:
|
||
return None
|
||
|
||
name = meta.get("name", "")
|
||
year = str(meta.get("year") or "").split("–")[0].split("-")[0]
|
||
|
||
# Détection anime approximative via genres Cinemeta
|
||
genres = [g.lower() for g in (meta.get("genres") or [])]
|
||
is_anime = "animation" in genres and (meta.get("country") or "").lower() in ("japan", "jp")
|
||
|
||
# Saisons reconstruites depuis la liste des épisodes (pour l'épisode absolu)
|
||
season_counts = {}
|
||
for video in meta.get("videos") or []:
|
||
s = video.get("season")
|
||
if s is not None and s > 0:
|
||
season_counts[s] = season_counts.get(s, 0) + 1
|
||
seasons = [{"season_number": s, "episode_count": c} for s, c in sorted(season_counts.items())]
|
||
|
||
return {
|
||
"tmdb_id": None,
|
||
"title": name,
|
||
"original_title": name,
|
||
"year": year,
|
||
"is_anime": is_anime,
|
||
"seasons": seasons,
|
||
}
|
||
|
||
|
||
def compute_absolute_episode(media_info, season, episode):
|
||
"""
|
||
Numérotation absolue pour les animes (nommage fansub "One Piece 1122").
|
||
Somme des épisodes des saisons précédentes + numéro dans la saison.
|
||
"""
|
||
if not media_info or not season or not episode:
|
||
return None
|
||
if season == 1:
|
||
return episode
|
||
prev = [
|
||
s for s in media_info.get("seasons", [])
|
||
if s.get("season_number") and 0 < s["season_number"] < season
|
||
]
|
||
if len(prev) == season - 1 and all(s.get("episode_count") for s in prev):
|
||
return sum(s["episode_count"] for s in prev) + episode
|
||
return None
|