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)}