mirror of
https://github.com/vatax3/ranger.git
synced 2026-07-27 05:02:09 +00:00
Feat: score qualité, cache liens résolus, mode d'affichage simplifié
- Score qualité global (résolution/source/HDR/codec/langue/seeders) +
critère de tri "quality" (⭐), sélectionnable et par défaut en 2e position.
Tri par défaut élargi à tous les critères (dont priorité tracker).
- Cache court des liens débrideur résolus (TTL 15 min, clé service+hash+SE+ip)
-> seek instantané, ~5x moins d'appels débrideur/StremThru. Table dédiée,
visible et purgeable dans l'admin.
- Mode d'affichage : "detailed" (actuel) ou "simple" (famille) = 1 lien par
résolution, meilleur choix auto (cache puis score qualité).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f873050b36
commit
b2233be562
9 changed files with 224 additions and 24 deletions
|
|
@ -21,8 +21,10 @@
|
|||
| **Filtres** | Taille min/max, résolution, codec, langue, exclusion CAM, exclusion packs, nombre de résultats (global + par résolution). |
|
||||
| **Tri multi-critères** | Cache → langue → résolution → taille → seeders → tracker, ordre entièrement réordonnable. |
|
||||
| **Déduplication** | Par info_hash, en gardant le tracker prioritaire et le max de seeders. |
|
||||
| **Cache-only / non-caché** | Afficher uniquement le caché, ou proposer les non-cachés (le clic les ajoute au débrideur). Option liens **P2P** (moteur torrent de Stremio, sans débrideur). |
|
||||
| **Cache SQLite** | Statut de cache débrideur, résultats de recherche et métadonnées TMDB mis en cache (TTLs configurables) → réponses quasi instantanées sur les épisodes suivants d'une série. |
|
||||
| **Cache-only / non-caché** | Afficher uniquement le caché, ou proposer les non-cachés (le clic les ajoute au débrideur). |
|
||||
| **Mode d'affichage** | *Détaillé* (tous les liens + infos, pour les initiés) ou *Simplifié* (1 choix par résolution, meilleur lien auto-sélectionné — pour la famille). |
|
||||
| **Score de qualité** | Tri « meilleur d'abord » holistique (résolution + source + HDR/DV + codec + langue + seeders), sélectionnable comme critère de tri. |
|
||||
| **Cache SQLite** | Statut de cache débrideur, résultats de recherche, métadonnées TMDB et **liens débrideur résolus** mis en cache (TTLs configurables) → épisodes suivants et seeks quasi instantanés. |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -103,6 +105,7 @@ Accessible sur `/admin` dès que `RANGER_ADMIN_TOKEN` est défini (sinon le pane
|
|||
| `RANGER_TTL_UNCACHED` | `1200` (20 min) | TTL cache « non caché » |
|
||||
| `RANGER_TTL_SEARCH` | `1800` (30 min) | TTL résultats de recherche |
|
||||
| `RANGER_TTL_META` | `604800` (7 j) | TTL métadonnées TMDB |
|
||||
| `RANGER_TTL_LINK` | `900` (15 min) | TTL des liens débrideur résolus (seek instantané) |
|
||||
| `HTTP_PROXY` / `HTTPS_PROXY` | — | Proxy sortant optionnel |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ TTL_AVAIL_CACHED = int(os.getenv("RANGER_TTL_CACHED", 6 * 3600)) # torrent
|
|||
TTL_AVAIL_MISS = int(os.getenv("RANGER_TTL_UNCACHED", 20 * 60)) # torrent vu non-caché
|
||||
TTL_SEARCH = int(os.getenv("RANGER_TTL_SEARCH", 30 * 60))
|
||||
TTL_META = int(os.getenv("RANGER_TTL_META", 7 * 24 * 3600))
|
||||
TTL_LINK = int(os.getenv("RANGER_TTL_LINK", 15 * 60)) # lien débrideur résolu
|
||||
|
||||
_lock = threading.Lock()
|
||||
_conn = None
|
||||
|
|
@ -60,6 +61,11 @@ def _get_conn():
|
|||
value TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS links (
|
||||
key TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
_conn.commit()
|
||||
|
|
@ -139,6 +145,29 @@ def set_search(key, results):
|
|||
conn.commit()
|
||||
|
||||
|
||||
def get_link(key):
|
||||
"""Lien débrideur résolu, encore valide (TTL court)."""
|
||||
now = int(time.time())
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
row = conn.execute("SELECT url, created_at FROM links WHERE key = ?", (key,)).fetchone()
|
||||
if row and now - row[1] <= TTL_LINK:
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
|
||||
def set_link(key, url):
|
||||
if not url:
|
||||
return
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO links (key, url, created_at) VALUES (?, ?, ?)",
|
||||
(key, url, int(time.time())),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_meta(key):
|
||||
now = int(time.time())
|
||||
with _lock:
|
||||
|
|
@ -170,6 +199,7 @@ def cleanup():
|
|||
conn.execute("DELETE FROM availability WHERE checked_at < ?", (now - max(TTL_AVAIL_CACHED, TTL_AVAIL_MISS),))
|
||||
conn.execute("DELETE FROM searches WHERE created_at < ?", (now - TTL_SEARCH,))
|
||||
conn.execute("DELETE FROM meta WHERE created_at < ?", (now - TTL_META,))
|
||||
conn.execute("DELETE FROM links WHERE created_at < ?", (now - TTL_LINK,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
|
|
@ -202,6 +232,7 @@ def stats():
|
|||
"FROM searches GROUP BY src ORDER BY COUNT(*) DESC"
|
||||
).fetchall()
|
||||
meta_total = conn.execute("SELECT COUNT(*) FROM meta").fetchone()[0]
|
||||
links_total = conn.execute("SELECT COUNT(*) FROM links").fetchone()[0]
|
||||
|
||||
db_size = os.path.getsize(DB_PATH) if os.path.exists(DB_PATH) else 0
|
||||
for wal in (DB_PATH + "-wal", DB_PATH + "-shm"):
|
||||
|
|
@ -224,11 +255,13 @@ def stats():
|
|||
"by_source": [{"source": s or "?", "total": t} for s, t in by_source],
|
||||
},
|
||||
"meta": {"total": meta_total},
|
||||
"links": {"total": links_total},
|
||||
"ttl": {
|
||||
"cached": TTL_AVAIL_CACHED,
|
||||
"uncached": TTL_AVAIL_MISS,
|
||||
"search": TTL_SEARCH,
|
||||
"meta": TTL_META,
|
||||
"link": TTL_LINK,
|
||||
},
|
||||
"metrics": get_metrics(),
|
||||
}
|
||||
|
|
@ -304,13 +337,13 @@ def refresh_media(imdb_id):
|
|||
|
||||
|
||||
def flush(table):
|
||||
"""Vide une table (searches|availability|meta|all)."""
|
||||
targets = ["searches", "availability", "meta"] if table == "all" else [table]
|
||||
"""Vide une table (searches|availability|meta|links|all)."""
|
||||
targets = ["searches", "availability", "meta", "links"] if table == "all" else [table]
|
||||
deleted = {}
|
||||
with _lock:
|
||||
conn = _get_conn()
|
||||
for t in targets:
|
||||
if t in ("searches", "availability", "meta"):
|
||||
if t in ("searches", "availability", "meta", "links"):
|
||||
deleted[t] = conn.execute(f"DELETE FROM {t}").rowcount
|
||||
conn.commit()
|
||||
return deleted
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ KEYED_TRACKERS = ["c411", "torr9", "tr4ker", "nekobt"]
|
|||
DEBRID_SERVICES = ["alldebrid", "realdebrid", "torbox", "debridlink"]
|
||||
|
||||
# Critères de tri disponibles
|
||||
SORT_CRITERIA = ["cached", "language", "resolution", "size_desc", "size_asc", "seeders", "tracker"]
|
||||
SORT_CRITERIA = ["cached", "quality", "language", "resolution", "size_desc", "size_asc", "seeders", "tracker"]
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
# Clé TMDB (recommandée : titres FR, détection anime, épisodes absolus).
|
||||
|
|
@ -32,6 +32,11 @@ DEFAULT_CONFIG = {
|
|||
# "all" : un stream par débrideur où le torrent est en cache
|
||||
"debrid_mode": "first",
|
||||
|
||||
# Mode d'affichage des résultats dans Stremio :
|
||||
# "detailed" : liste complète (tous les liens + détails) — pour les initiés
|
||||
# "simple" : 1 lien par résolution, meilleur choix auto — pour la famille
|
||||
"display_mode": "detailed",
|
||||
|
||||
# StremThru : proxy d'API débrideur (contourne les blocages IP datacenter)
|
||||
"stremthru": {"url": "", "auth": ""},
|
||||
|
||||
|
|
@ -67,7 +72,7 @@ DEFAULT_CONFIG = {
|
|||
},
|
||||
|
||||
# Ordre des critères de tri (appliqués dans l'ordre)
|
||||
"sort": ["cached", "language", "resolution", "size_desc"],
|
||||
"sort": ["cached", "quality", "resolution", "language", "size_desc", "seeders", "tracker"],
|
||||
|
||||
# Priorités utilisées par les critères "language" / "resolution" / "tracker"
|
||||
"language_order": ["MULTI", "VFF", "VF", "VFQ", "VOSTFR", "VO"],
|
||||
|
|
|
|||
|
|
@ -121,6 +121,31 @@ def build_debrid_stream(torrent, service, cached, resolve_url):
|
|||
return stream
|
||||
|
||||
|
||||
def build_simple_stream(torrent, service, cached, resolve_url, resolution):
|
||||
"""
|
||||
Stream du mode simplifié (famille) : un seul lien par résolution, présenté
|
||||
de façon épurée. Le meilleur torrent a déjà été choisi en amont.
|
||||
"""
|
||||
meta = torrent["_meta"]
|
||||
label = resolution or "Auto"
|
||||
dot = "🟢" if cached else "🟠"
|
||||
lang = meta["lang_display"] or ", ".join(meta["languages"])
|
||||
infos = [f"💾 {format_size(torrent.get('size', 0))}"]
|
||||
if lang:
|
||||
infos.insert(0, lang)
|
||||
if not cached:
|
||||
infos.append("ajout au débrideur")
|
||||
|
||||
return {
|
||||
"name": f"Ranger\n{dot} {label}",
|
||||
"title": " · ".join(infos),
|
||||
"description": " · ".join(infos),
|
||||
"url": resolve_url,
|
||||
"behaviorHints": _behavior_hints(torrent, service),
|
||||
**_stream_meta_fields(torrent),
|
||||
}
|
||||
|
||||
|
||||
def build_p2p_stream(torrent):
|
||||
"""Stream P2P via le moteur torrent intégré de Stremio (sans débrideur)."""
|
||||
meta = torrent["_meta"]
|
||||
|
|
|
|||
|
|
@ -139,3 +139,50 @@ def resolution_rank(resolution, resolution_order):
|
|||
return resolution_order.index(resolution)
|
||||
except ValueError:
|
||||
return len(resolution_order)
|
||||
|
||||
|
||||
# --- Score de qualité global (plus élevé = meilleur) ---
|
||||
|
||||
_RES_SCORE = {"4K": 4000, "1080p": 3000, "720p": 2000, "SD": 1000}
|
||||
_SOURCE_SCORE = {
|
||||
"REMUX": 500, "BluRay": 400, "WEB-DL": 350, "WEBRip": 250,
|
||||
"WEB": 220, "HDTV": 120, "DVDRip": 100, "CAM": -2000,
|
||||
}
|
||||
|
||||
|
||||
def quality_score(meta, seeders=0, language_order=None):
|
||||
"""
|
||||
Score de qualité d'une release, indépendant du statut de cache.
|
||||
Combine résolution, source, HDR/DV, codec, langue préférée et seeders.
|
||||
"""
|
||||
score = 0
|
||||
score += _RES_SCORE.get(meta.get("resolution"), 500)
|
||||
score += _SOURCE_SCORE.get(meta.get("source"), 150)
|
||||
|
||||
hdr = meta.get("hdr") or []
|
||||
if "DV" in hdr:
|
||||
score += 120
|
||||
if any(h.startswith("HDR") for h in hdr):
|
||||
score += 80
|
||||
if "10bit" in hdr:
|
||||
score += 30
|
||||
|
||||
if meta.get("codec") == "x265":
|
||||
score += 40 # meilleur ratio qualité/taille
|
||||
elif meta.get("codec") == "AV1":
|
||||
score += 50
|
||||
|
||||
# Langue préférée : bonus décroissant selon l'ordre de préférence
|
||||
if language_order:
|
||||
rank = best_language(meta.get("languages") or [], language_order)
|
||||
score += max(0, (len(language_order) - rank)) * 25
|
||||
|
||||
# Seeders : petit bonus logarithmique (fiabilité), plafonné
|
||||
try:
|
||||
s = int(seeders or 0)
|
||||
if s > 0:
|
||||
score += min(200, int((s ** 0.5) * 10))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return score
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Moteur de filtrage, déduplication et tri des torrents.
|
|||
|
||||
import logging
|
||||
|
||||
from core.parsing import parse_release, best_language, resolution_rank
|
||||
from core.parsing import parse_release, best_language, resolution_rank, quality_score
|
||||
from utils import (
|
||||
check_title_match,
|
||||
check_title_tokens,
|
||||
|
|
@ -129,6 +129,15 @@ def dedupe(torrents, providers_order):
|
|||
return list(unique.values())
|
||||
|
||||
|
||||
def score_of(torrent, language_order):
|
||||
"""Score qualité mémoïsé sur le torrent."""
|
||||
if "_score" not in torrent:
|
||||
torrent["_score"] = quality_score(
|
||||
torrent["_meta"], torrent.get("seeders", 0), language_order
|
||||
)
|
||||
return torrent["_score"]
|
||||
|
||||
|
||||
def sort_entries(entries, config):
|
||||
"""
|
||||
Trie les entrées de stream selon les critères configurés (dans l'ordre).
|
||||
|
|
@ -146,6 +155,8 @@ def sort_entries(entries, config):
|
|||
for criterion in criteria:
|
||||
if criterion == "cached":
|
||||
parts.append(0 if entry["cached"] else 1)
|
||||
elif criterion == "quality":
|
||||
parts.append(-score_of(t, lang_order))
|
||||
elif criterion == "language":
|
||||
parts.append(best_language(meta["languages"], lang_order))
|
||||
elif criterion == "resolution":
|
||||
|
|
|
|||
84
main.py
84
main.py
|
|
@ -235,15 +235,17 @@ async def handle_stream(request):
|
|||
availability[backend.name] = avail
|
||||
logging.info(f"{backend.name}: {sum(1 for v in avail.values() if v)}/{len(hashes)} en cache")
|
||||
|
||||
# 5. Construction des entrées (torrent x débrideur)
|
||||
entries = _build_entries(torrents, backends, availability, config)
|
||||
# 5-7. Construction des streams selon le mode d'affichage
|
||||
if config.get("display_mode") == "simple":
|
||||
# Mode famille : 1 lien par résolution, meilleur choix automatique
|
||||
entries = _build_simple_entries(torrents, backends, availability, config)
|
||||
streams = _serialize_streams(entries, config_str, host_url, stream_type, season, episode, simple=True)
|
||||
else:
|
||||
entries = _build_entries(torrents, backends, availability, config)
|
||||
entries = ranking.sort_entries(entries, config)
|
||||
entries = ranking.apply_limits(entries, filters)
|
||||
streams = _serialize_streams(entries, config_str, host_url, stream_type, season, episode)
|
||||
|
||||
# 6. Tri + limites
|
||||
entries = ranking.sort_entries(entries, config)
|
||||
entries = ranking.apply_limits(entries, filters)
|
||||
|
||||
# 7. Sérialisation en streams Stremio
|
||||
streams = _serialize_streams(entries, config_str, host_url, stream_type, season, episode)
|
||||
RUNTIME["streams_served"] += len(streams)
|
||||
logging.info(f"{len(streams)} streams renvoyés à Stremio")
|
||||
return web.json_response({"streams": streams})
|
||||
|
|
@ -282,7 +284,51 @@ def _build_entries(torrents, backends, availability, config):
|
|||
return entries
|
||||
|
||||
|
||||
def _serialize_streams(entries, config_str, host_url, stream_type, season, episode):
|
||||
def _build_simple_entries(torrents, backends, availability, config):
|
||||
"""
|
||||
Mode simplifié : une seule entrée par résolution, le meilleur torrent
|
||||
(cache d'abord, puis score qualité) sur le débrideur prioritaire dispo.
|
||||
"""
|
||||
filters = config.get("filters") or {}
|
||||
cached_only = filters.get("cached_only", False)
|
||||
show_uncached = filters.get("show_uncached", True) and not cached_only
|
||||
lang_order = config.get("language_order") or []
|
||||
res_order = config.get("resolution_order") or ["4K", "1080p", "720p", "SD"]
|
||||
|
||||
# Candidats : (torrent, service, cached) au meilleur débrideur possible
|
||||
candidates = []
|
||||
for t in torrents:
|
||||
ih = t["info_hash"]
|
||||
cached_backends = [b for b in backends if availability.get(b.name, {}).get(b.clean_hash(ih), False)]
|
||||
if cached_backends:
|
||||
candidates.append({"torrent": t, "service": cached_backends[0].name, "cached": True})
|
||||
elif backends and show_uncached:
|
||||
candidates.append({"torrent": t, "service": backends[0].name, "cached": False})
|
||||
|
||||
# Groupement par résolution, meilleur candidat par groupe
|
||||
best_by_res = {}
|
||||
for c in candidates:
|
||||
res = c["torrent"]["_meta"]["resolution"] or "Auto"
|
||||
rank = (0 if c["cached"] else 1, -ranking.score_of(c["torrent"], lang_order))
|
||||
if res not in best_by_res or rank < best_by_res[res][0]:
|
||||
best_by_res[res] = (rank, c)
|
||||
|
||||
# Ordonné selon la préférence de résolution
|
||||
def res_key(res):
|
||||
try:
|
||||
return res_order.index(res)
|
||||
except ValueError:
|
||||
return len(res_order)
|
||||
|
||||
entries = []
|
||||
for res in sorted(best_by_res, key=res_key):
|
||||
entry = best_by_res[res][1]
|
||||
entry["resolution"] = res
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
def _serialize_streams(entries, config_str, host_url, stream_type, season, episode, simple=False):
|
||||
streams = []
|
||||
for entry in entries:
|
||||
t = entry["torrent"]
|
||||
|
|
@ -297,7 +343,11 @@ def _serialize_streams(entries, config_str, host_url, stream_type, season, episo
|
|||
elif stream_type == "movie":
|
||||
resolve_url += "?type=movie"
|
||||
|
||||
streams.append(formatting.build_debrid_stream(t, service, entry["cached"], resolve_url))
|
||||
if simple:
|
||||
streams.append(formatting.build_simple_stream(
|
||||
t, service, entry["cached"], resolve_url, entry.get("resolution", "")))
|
||||
else:
|
||||
streams.append(formatting.build_debrid_stream(t, service, entry["cached"], resolve_url))
|
||||
return streams
|
||||
|
||||
|
||||
|
|
@ -316,8 +366,19 @@ async def handle_resolve(request):
|
|||
season = request.query.get("season")
|
||||
episode = request.query.get("episode")
|
||||
media_type = request.query.get("type")
|
||||
client_ip = get_client_ip(request)
|
||||
|
||||
backends = build_backends(config, client_ip=get_client_ip(request))
|
||||
# Cache court des liens résolus : évite de re-débrider à chaque seek
|
||||
# (libmpv rappelle /resolve à chaque Range request). Clé liée à l'IP client
|
||||
# car certains liens débrideur sont générés pour une IP donnée.
|
||||
link_key = f"{service_name}|{info_hash}|{season or ''}|{episode or ''}|{client_ip or ''}"
|
||||
cached_url = cache.get_link(link_key)
|
||||
if cached_url:
|
||||
RUNTIME["resolve_ok"] += 1
|
||||
logging.info(f"⚡ Lien résolu servi depuis le cache ({service_name})")
|
||||
raise web.HTTPFound(cached_url)
|
||||
|
||||
backends = build_backends(config, client_ip=client_ip)
|
||||
backend = next((b for b in backends if b.name == service_name), None)
|
||||
if not backend:
|
||||
return web.Response(status=400, text=f"Débrideur non configuré : {service_name}")
|
||||
|
|
@ -330,6 +391,7 @@ async def handle_resolve(request):
|
|||
)
|
||||
if url:
|
||||
RUNTIME["resolve_ok"] += 1
|
||||
cache.set_link(link_key, url)
|
||||
raise web.HTTPFound(url)
|
||||
return web.Response(status=404, text="Impossible de résoudre le stream")
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@
|
|||
<button class="btn amber sm" onclick="flush('searches')">Vider les recherches</button>
|
||||
<button class="btn amber sm" onclick="flush('availability')">Vider la dispo débrideur</button>
|
||||
<button class="btn amber sm" onclick="flush('meta')">Vider les métadonnées</button>
|
||||
<button class="btn amber sm" onclick="flush('links')">Vider les liens résolus</button>
|
||||
<button class="btn red sm" onclick="flush('all')">⚠️ Tout vider</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -230,6 +231,7 @@ function renderStats(s) {
|
|||
${stat(c.availability.total, "Hashes en cache", c.availability.cached + " cachés / " + c.availability.uncached + " non")}
|
||||
${stat(c.searches.total, "Recherches en cache")}
|
||||
${stat(c.meta.total, "Métadonnées")}
|
||||
${stat((c.links && c.links.total) || 0, "Liens résolus en cache")}
|
||||
${stat(m.search_hit_rate + "%", "Hit-rate recherche", m.search_hit + " hits / " + m.search_miss + " miss")}
|
||||
${stat(m.avail_hit_rate + "%", "Hit-rate dispo", m.avail_hit + " hits / " + m.avail_miss + " miss")}
|
||||
`;
|
||||
|
|
@ -301,7 +303,7 @@ async function delKey(table, key) {
|
|||
} catch (e) { toast(e.message, false); }
|
||||
}
|
||||
async function flush(table) {
|
||||
const labels = { searches:"les recherches", availability:"la dispo débrideur", meta:"les métadonnées", all:"TOUT le cache" };
|
||||
const labels = { searches:"les recherches", availability:"la dispo débrideur", meta:"les métadonnées", links:"les liens résolus", all:"TOUT le cache" };
|
||||
if (!confirm(`Vider ${labels[table]} ? Cette action est irréversible.`)) return;
|
||||
try {
|
||||
const r = await api("/admin/api/flush", { method:"POST", body: JSON.stringify({ table }) });
|
||||
|
|
|
|||
|
|
@ -124,6 +124,16 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Mode d'affichage -->
|
||||
<div class="card">
|
||||
<h2>🖥️ Mode d'affichage</h2>
|
||||
<p class="hint">Comment les résultats apparaissent dans Stremio.</p>
|
||||
<select id="display_mode">
|
||||
<option value="detailed">Détaillé — tous les liens et détails (pour les initiés)</option>
|
||||
<option value="simple">Simplifié — 1 choix par résolution, meilleur lien auto (famille)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- StremThru -->
|
||||
<div class="card">
|
||||
<h2>🛰️ StremThru <span class="small">(optionnel)</span></h2>
|
||||
|
|
@ -258,8 +268,8 @@ const RESOLUTIONS = ["4K", "1080p", "720p", "SD"];
|
|||
const CODECS = ["x265", "x264", "AV1"];
|
||||
const LANGUAGES = ["MULTI", "VFF", "VF", "VFQ", "VOSTFR", "VO"];
|
||||
const SORT_LABELS = {
|
||||
cached: "⚡ En cache d'abord", language: "🗣️ Langue", resolution: "📺 Résolution",
|
||||
size_desc: "💾 Taille décroissante", size_asc: "💾 Taille croissante",
|
||||
cached: "⚡ En cache d'abord", quality: "⭐ Qualité globale", language: "🗣️ Langue",
|
||||
resolution: "📺 Résolution", size_desc: "💾 Taille décroissante", size_asc: "💾 Taille croissante",
|
||||
seeders: "👤 Seeders", tracker: "🌐 Priorité tracker",
|
||||
};
|
||||
// Noms lisibles des sources (priorité d'affichage des trackers)
|
||||
|
|
@ -275,7 +285,7 @@ const PREFILL = null;
|
|||
let debrids = []; // [{service, key}]
|
||||
let unit3ds = []; // [{url, key}]
|
||||
let torznabs = []; // [{name, url, apikey}]
|
||||
let sortOrder = ["cached", "language", "resolution", "size_desc"];
|
||||
let sortOrder = ["cached", "quality", "resolution", "language", "size_desc", "seeders", "tracker"];
|
||||
let langOrder = [...LANGUAGES];
|
||||
let provOrder = ["unit3d", "ygg", "c411", "torr9", "tr4ker", "abn", "torznab", "eztv", "apibay", "nyaa", "nekobt"];
|
||||
const selectedRes = new Set(), selectedCodec = new Set(), selectedLang = new Set();
|
||||
|
|
@ -446,6 +456,7 @@ function buildConfig() {
|
|||
tmdb_key: document.getElementById("tmdb_key").value.trim(),
|
||||
debrids: debrids.filter(d => d.key && d.key.trim()).map(d => ({ service: d.service, key: d.key.trim(), stremthru: d.stremthru !== false })),
|
||||
debrid_mode: document.getElementById("debrid_mode").value,
|
||||
display_mode: document.getElementById("display_mode").value,
|
||||
stremthru: {
|
||||
url: document.getElementById("stremthru_url").value.trim(),
|
||||
auth: document.getElementById("stremthru_auth").value.trim(),
|
||||
|
|
@ -534,6 +545,7 @@ function applyPrefill(cfg) {
|
|||
// Champs simples
|
||||
setVal("tmdb_key", cfg.tmdb_key);
|
||||
setVal("debrid_mode", cfg.debrid_mode);
|
||||
setVal("display_mode", cfg.display_mode);
|
||||
if (cfg.stremthru) { setVal("stremthru_url", cfg.stremthru.url); setVal("stremthru_auth", cfg.stremthru.auth); }
|
||||
if (cfg.abn) { setVal("abn_user", cfg.abn.username); setVal("abn_pass", cfg.abn.password); }
|
||||
setVal("min_size", f.min_size_gb); setVal("max_size", f.max_size_gb);
|
||||
|
|
@ -576,7 +588,7 @@ renderSortable("sortList", sortOrder, k => SORT_LABELS[k] || k);
|
|||
renderSortable("langOrder", langOrder, k => k);
|
||||
renderSortable("provOrder", provOrder, k => SOURCE_LABELS[k] || k);
|
||||
applyPrefillCheckboxes(PREFILL);
|
||||
["tmdb_key","stremthru_url","stremthru_auth","abn_user","abn_pass","debrid_mode",
|
||||
["tmdb_key","stremthru_url","stremthru_auth","abn_user","abn_pass","debrid_mode","display_mode",
|
||||
"min_size","max_size","max_results","max_per_res","exclude_cam","exclude_packs",
|
||||
"cache_mode"].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
|
|
|
|||
Loading…
Reference in a new issue