def check_file(file_path: str, chat_id: int, item_id: int, app) -> dict:
"""
- Se originale <= limite: original_ok.
- Altrimenti:
Tentativo 1: calcolo plan con limite "fudgiato" (solo qui).
Se OK (<= limite reale): replace e encoded_ok.
Se overshoot e RETRY_ON_OVERSHOOT=True: Tentativo 2.
- Mantiene audio e vf_height del 1°
- Ricalcola SOLO v_kbps 'spalmando' l'eccesso + safety.
Se 2° OK: replace e encoded_ok.
Altrimenti: too_big (lo split lo farà chi chiama).
"""
try:
src = Path(file_path)
if not src.exists():
msg = "❌ File non trovato"
try: send_telegram_message(chat_id, msg)
except Exception: pass
return {"status": "error", "msg": msg, "path": None}
orig_size = src.stat().st_size
if orig_size <= safe_limit_bytes:
# logger.info("[check] Originale sotto soglia: %s <= %s", orig_size, safe_limit_bytes)
logger.info(
"[check] originale <= limite: size=%s (%d B) <= limit=%s (%d B) — skip encode.",
_fmt_bytes(orig_size), orig_size, _fmt_bytes(safe_limit_bytes), safe_limit_bytes
)
return {"status": "original_ok", "path": str(src), "size": orig_size}
if app == "sonarr":
api_key = sonarr_api
host = sonarr_host
port = sonarr_port
elif app == "radarr":
api_key = radarr_api
host = radarr_host
port = radarr_port
else:
raise ValueError("app must be 'radarr' or 'sonarr'")
profile_language = get_profile_language_iso639_2(app, api_key, host, port, item_id)
langs = _analyze_audio_languages_with_docker(src)
filter_enabled, keep_relidx, notify_msg, summary_str = _decide_audio_selection(profile_language, langs)
# Log elenco tracce rilevate
logger.info("[audio] Tracce: %s", summary_str)
# Notifiche all'owner, se richieste dalla logica
if notify_msg:
try:
send_telegram_message(owner_chatid, f"ℹ️ {src.name}: {notify_msg}")
except Exception:
pass
ctx = extract_encode_context(str(src))
# Report keep/remove
if filter_enabled:
keep_1based = [i+1 for i in sorted(keep_relidx)]
all_1based = [int(t.get("track")) for t in langs]
remove_1based = [t for t in all_1based if t not in keep_1based]
# logger.info("[audio] Keep: %s | Remove: %s",
# ", ".join(f"#{i}" for i in keep_1based) if keep_1based else "(none)",
# ", ".join(f"#{i}" for i in remove_1based) if remove_1based else "(none)")
logger.info(
"[audio] selection: keep=%s (%d/%d), remove=%s",
", ".join(f"#{i}" for i in keep_1based) if keep_1based else "(none)",
len(keep_1based), len(langs),
", ".join(f"#{i}" for i in remove_1based) if remove_1based else "(none)"
)
else:
logger.info("[audio] selection: keep=all (%d/%d), remove=(none)", ctx.n_audio, ctx.n_audio)
# Context e output temporaneo
# ctx = extract_encode_context(str(src))
out_tmp = str(src.with_name(f"{src.stem}.encode{REENCODE_OUTPUT_EXT}"))
try:
if os.path.exists(out_tmp):
os.remove(out_tmp)
except Exception:
pass
dur_txt = _fmt_hms(ctx.duration_s)
try:
send_telegram_message(
chat_id,
"🗜️ Provo compressione GPU (HEVC NVENC, multipass)…"
)
logger.info(
"[check] source: name=%s, dur=%s (%.0fs), size=%s (%d B), limit=%s (%d B), app=%s, profile_lang=%s",
src.name, _fmt_hms(ctx.duration_s), ctx.duration_s,
_fmt_bytes(orig_size), orig_size,
_fmt_bytes(safe_limit_bytes), safe_limit_bytes,
app, profile_language or "n/a"
)
a_src_est = max(0, ctx.source_total_kbps_est - ctx.source_video_kbps_est)
logger.info(
"[src] video: %dx%d, %s-bit, v_src≈%d kb/s",
ctx.width, ctx.height, ("10" if ctx.is_10bit else "8"), ctx.source_video_kbps_est
)
logger.info(
"[src] audio: tracks=%d, a_src_est≈%d kb/s",
ctx.n_audio, a_src_est
)
logger.info(
"[src] container: total_src≈%d kb/s, dur=%s (%.0fs), size=%s (%d B), ext=%s",
ctx.source_total_kbps_est, dur_txt, ctx.duration_s,
_fmt_bytes(orig_size), orig_size, ctx.ext_low
)
except Exception:
pass
# ---- Tentativo 1 (fudge -1% sul limite totale) ----
limit_try1 = int(safe_limit_bytes * (1.0 - FUDGE_VBR_FIRST_TRY))
n_audio_eff = len(keep_relidx) if filter_enabled else ctx.n_audio
plan1 = compute_target_for_size(ctx, limit_try1, n_audio_override=n_audio_eff)
# logger.info("[check] Tentativo 1 → target_kbps_tot=%s, audio=%s, video=%s, vf_h=%s",
# plan1.safe_total_kbps, plan1.target_audio_kbps_total, plan1.v_kbps, plan1.vf_height)
logger.info(
"[plan] try#1: total=%s kb/s (v=%s, a=%s), vf_h=%s, limit_fudged=%s (%d B, fudge=%.2f%%), dur=%s (%.0fs)",
plan1.safe_total_kbps, plan1.v_kbps, plan1.target_audio_kbps_total, plan1.vf_height,
_fmt_bytes(limit_try1), limit_try1, (FUDGE_VBR_FIRST_TRY * 100.0),
_fmt_hms(ctx.duration_s), ctx.duration_s
)
# Se filtriamo, passiamo la mappa esplicita; altrimenti None per comportamento corrente
audio_map = keep_relidx if filter_enabled else None
rc1, el1, out1 = run_encode_once(out_tmp, ctx, plan1, audio_map_relidx=audio_map)
logger.info("[encode] v_kbps=%s, vf_h=%s, audio_kbps_total=%s, safe_total_kbps=%s, src_v_kbps=%s",
plan1.v_kbps, plan1.vf_height, plan1.target_audio_kbps_total,
plan1.safe_total_kbps, getattr(plan1, "source_video_kbps_est", "n/a"))
if rc1 != 0 or out1 <= 0 or not os.path.exists(out_tmp):
try: send_telegram_message(chat_id, "❌ Compress. fallita (1° tentativo).")
except Exception: pass
try:
if os.path.exists(out_tmp):
os.remove(out_tmp)
except Exception:
pass
return {"status": "error", "path": str(src), "msg": "encode1_failed"}
logger.info("[encode] OK rc=%s, size=%s bytes", rc1, out1)
if out1 <= safe_limit_bytes:
# SUCCESSO → sostituisco l'originale
try:
dest_path = src.with_suffix(REENCODE_OUTPUT_EXT)
os.replace(out_tmp, dest_path)
if dest_path != src:
try:
os.remove(src)
except Exception:
pass
try:
eff_n_audio = (len(audio_map) if audio_map is not None else ctx.n_audio)
update_mkv_metadata_final(dest_path, src, eff_n_audio)
logger.info("[final] mkvpropedit: track statistics tags rigenerati + segment title/default audio impostati.")
except Exception:
pass
final_size = os.path.getsize(dest_path)
try:
eff_total_kbps_1 = int((final_size * 8) / max(1.0, ctx.duration_s) / 1000)
margin_pct_1 = (1.0 - (final_size / safe_limit_bytes)) * 100.0
logger.info(
"[final] try#1 OK: size=%s (%d B) ≤ limit=%s (%d B), margin=%.2f%%, "
"eff_total=%d kb/s vs plan_total=%s, v=%s, vf_h=%s, audio_streams=%d, dest=%s",
_fmt_bytes(final_size), final_size,
_fmt_bytes(safe_limit_bytes), safe_limit_bytes, margin_pct_1,
eff_total_kbps_1, plan1.safe_total_kbps, plan1.v_kbps, plan1.vf_height,
(len(audio_map) if filter_enabled else ctx.n_audio),
str(dest_path)
)
size_factor = (orig_size / final_size) if final_size > 0 else 0.0
size_redux_pct = 100.0 * (1.0 - (final_size / orig_size))
tot_redux_pct = 100.0 * (1.0 - (eff_total_kbps_1 / max(1, ctx.source_total_kbps_est)))
v_redux_pct = 100.0 * (1.0 - (plan1.v_kbps / max(1, ctx.source_video_kbps_est)))
logger.info(
"[final] delta#1: total≈%d→%d kb/s (Δ=%.2f%%), v≈%d→%d kb/s (plan, Δ=%.2f%%), "
"size %s→%s (x%.2f, Δ=%.2f%%), vf_h=%s, a_streams=%d",
ctx.source_total_kbps_est, eff_total_kbps_1, tot_redux_pct,
ctx.source_video_kbps_est, plan1.v_kbps, v_redux_pct,
_fmt_bytes(orig_size), _fmt_bytes(final_size), size_factor, size_redux_pct,
plan1.vf_height, (len(audio_map) if filter_enabled else ctx.n_audio)
)
except Exception:
pass
except Exception:
try: os.remove(out_tmp)
except Exception: pass
final_size = out1 # best effort
try:
send_telegram_message(chat_id, f"✅ Compressione ok: {_fmt_bytes(final_size)} (limite {_fmt_bytes(safe_limit_bytes)})")
except Exception:
pass
# return {"status": "encoded_ok", "path": file_path, "size": final_size,
return {"status": "encoded_ok", "path": dest_path, "size": final_size,
"plan": {"vf_height": plan1.vf_height, "v_kbps": plan1.v_kbps,
"audio_kbps": plan1.target_audio_kbps_total,
"safe_total_kbps": plan1.safe_total_kbps,
"src_video_kbps_est": getattr(plan1, "source_video_kbps_est", None)}}
# ---- Overshoot → valutazione retry ----
if not RETRY_ON_OVERSHOOT:
try:
send_telegram_message(chat_id, f"ℹ️ Output {_fmt_bytes(out1)} > limite ({_fmt_bytes(safe_limit_bytes)}). Retry disattivato.")
logger.info(
"[overshoot] try#1: size=%s (%d B) > limit=%s (%d B) by %.2f%% — retry disattivato.",
_fmt_bytes(out1), out1, _fmt_bytes(safe_limit_bytes), safe_limit_bytes,
((out1 - safe_limit_bytes) / safe_limit_bytes) * 100.0
)
except Exception:
pass
try:
if os.path.exists(out_tmp):
os.remove(out_tmp)
except Exception:
pass
return {"status": "too_big", "path": str(src), "size": out1, "msg": "overshoot_no_retry"}
overs_bytes = out1 - safe_limit_bytes
pct = (overs_bytes / safe_limit_bytes) * 100.0
min_floor = _floor_value_min(H265_FLOOR_KBPS)
v2 = _compute_retry_v_kbps(
ctx.duration_s, out1, safe_limit_bytes, plan1.v_kbps, RETRY_SAFETY_PCT,
floor_kbps=min_floor
)
try:
plan2 = adjust_plan_after_retry(ctx, plan1, v2) # usa H265_FLOOR_KBPS e MAX_TARGET_HEIGHT interni/globali
except EncodeNotFeasible as e:
# Retry non fattibile (v2 < floor minore, o nessun bucket compatibile) → chiedi split
try:
send_telegram_message(chat_id,
f"❌ Retry impossibile: {e}. Suggerito split.")
except Exception:
pass
try:
if os.path.exists(out_tmp):
os.remove(out_tmp)
except Exception:
pass
return {"status": "too_big", "path": str(src), "size": out1, "msg": "overshoot_retry_unfeasible"}
try:
send_telegram_message(
chat_id,
"ℹ️ Overshoot: {} > limite ({}) di {:.2f}%.\n"
"Retry: video {} → {} kb/s; risoluzione {} → {}."
.format(_fmt_bytes(out1), _fmt_bytes(safe_limit_bytes), pct,
plan1.v_kbps, plan2.v_kbps, plan1.vf_height, plan2.vf_height)
)
logger.info(
"[overshoot] try#1: size=%s (%d B) > limit=%s (%d B) by %.2f%% — "
"retry plan: v=%s→%s kb/s, vf_h=%s→%s",
_fmt_bytes(out1), out1, _fmt_bytes(safe_limit_bytes), safe_limit_bytes, pct,
plan1.v_kbps, plan2.v_kbps, plan1.vf_height, plan2.vf_height
)
except Exception:
pass
logger.info(
"[plan] try#2: total=%s kb/s (v=%s, a=%s), vf_h=%s",
plan2.safe_total_kbps, plan2.v_kbps, plan2.target_audio_kbps_total, plan2.vf_height
)
rc2, el2, out2 = run_encode_once(out_tmp, ctx, plan2, audio_map_relidx=audio_map)
logger.info("[encode] v_kbps=%s, vf_h=%s, audio_kbps_total=%s, safe_total_kbps=%s, src_v_kbps=%s",
plan2.v_kbps, plan2.vf_height, plan2.target_audio_kbps_total,
plan2.safe_total_kbps, getattr(plan2, "source_video_kbps_est", "n/a"))
if rc2 != 0 or out2 <= 0 or not os.path.exists(out_tmp):
try: send_telegram_message(chat_id, "❌ Compress. fallita (2° tentativo).")
except Exception: pass
try:
if os.path.exists(out_tmp):
os.remove(out_tmp)
except Exception:
pass
return {"status": "error", "path": str(src), "msg": "encode2_failed"}
logger.info("[encode] OK rc=%s, size=%s bytes", rc2, out2)
if out2 <= safe_limit_bytes:
try:
dest_path = src.with_suffix(REENCODE_OUTPUT_EXT)
os.replace(out_tmp, dest_path)
if dest_path != src:
try:
os.remove(src)
except Exception:
pass
try:
eff_n_audio = (len(audio_map) if audio_map is not None else ctx.n_audio)
update_mkv_metadata_final(dest_path, src, eff_n_audio)
logger.info("[final] mkvpropedit: track statistics tags rigenerati + segment title/default audio impostati.")
except Exception:
pass
final_size = os.path.getsize(dest_path)
try:
eff_total_kbps_2 = int((final_size * 8) / max(1.0, ctx.duration_s) / 1000)
margin_pct_2 = (1.0 - (final_size / safe_limit_bytes)) * 100.0
logger.info(
"[final] try#2 OK: size=%s (%d B) ≤ limit=%s (%d B), margin=%.2f%%, "
"eff_total=%d kb/s vs plan_total=%s, v=%s, vf_h=%s, audio_streams=%d, dest=%s",
_fmt_bytes(final_size), final_size,
_fmt_bytes(safe_limit_bytes), safe_limit_bytes, margin_pct_2,
eff_total_kbps_2, plan2.safe_total_kbps, plan2.v_kbps, plan2.vf_height,
(len(audio_map) if filter_enabled else ctx.n_audio),
str(dest_path)
)
size_factor = (orig_size / final_size) if final_size > 0 else 0.0
size_redux_pct = 100.0 * (1.0 - (final_size / orig_size))
tot_redux_pct = 100.0 * (1.0 - (eff_total_kbps_2 / max(1, ctx.source_total_kbps_est)))
v_redux_pct = 100.0 * (1.0 - (plan2.v_kbps / max(1, ctx.source_video_kbps_est)))
logger.info(
"[final] delta#2: total≈%d→%d kb/s (Δ=%.2f%%), v≈%d→%d kb/s (plan, Δ=%.2f%%), "
"size %s→%s (x%.2f, Δ=%.2f%%), vf_h=%s, a_streams=%d",
ctx.source_total_kbps_est, eff_total_kbps_2, tot_redux_pct,
ctx.source_video_kbps_est, plan2.v_kbps, v_redux_pct,
_fmt_bytes(orig_size), _fmt_bytes(final_size), size_factor, size_redux_pct,
plan2.vf_height, (len(audio_map) if filter_enabled else ctx.n_audio)
)
except Exception:
pass
except Exception:
try: os.remove(out_tmp)
except Exception: pass
final_size = out2
try:
send_telegram_message(chat_id, f"✅ Compressione ok al 2° tentativo: {_fmt_bytes(final_size)} (limite {_fmt_bytes(safe_limit_bytes)})")
except Exception:
pass
# return {"status": "encoded_ok", "path": file_path, "size": final_size,
return {"status": "encoded_ok", "path": dest_path, "size": final_size,
"plan": {"vf_height": plan2.vf_height, "v_kbps": plan2.v_kbps,
"audio_kbps": plan2.target_audio_kbps_total,
"safe_total_kbps": plan2.safe_total_kbps,
"src_video_kbps_est": getattr(plan2, "source_video_kbps_est", None)}}
# Ancora troppo grande → lasciare la decisione di split al chiamante
try:
if os.path.exists(out_tmp):
os.remove(out_tmp)
except Exception:
pass
try:
send_telegram_message(chat_id, f"ℹ️ Output ancora > limite ({_fmt_bytes(safe_limit_bytes)}).")
except Exception:
pass
logger.info(
"[final] try#2 still too big: size=%s (%d B) > limit=%s (%d B) by %.2f%% — split demandato al chiamante.",
_fmt_bytes(out2), out2, _fmt_bytes(safe_limit_bytes), safe_limit_bytes,
((out2 - safe_limit_bytes) / safe_limit_bytes) * 100.0
)
return {"status": "too_big", "path": str(src), "size": out2, "msg": "overshoot_after_retry"}
except Exception as e:
logger.exception("[check_file] eccezione: %s", e)
try: send_telegram_message(chat_id, f"❌ Errore check_file: {e}")
except Exception: pass
return {"status": "error", "path": None, "msg": str(e)}