CCCPaste

1d79cdc685fed

def _compute_bpp_proxy(ctx: EncodeContext) -> float:
    """
    Proxy estremamente semplificato del "bitrate per pixel".
    Restituisce kbps per megapixel circa (solo per logging/euristica).
    """
    pixels = max(1, ctx.width * ctx.height)
    mpix = pixels / 1_000_000.0
    return ctx.source_video_kbps_est / mpix if mpix > 0 else ctx.source_video_kbps_est


def classify_complexity_heuristic(ctx: EncodeContext) -> Complexity:
    """
    Classificazione grossolana LOW/MID/HIGH basata sul "kbps per megapixel".
    DA SOSTITUIRE/RAFFINARE con SI/TI + VMAF nei passi successivi.
    """
    bpp_proxy = _compute_bpp_proxy(ctx)
    # Soglie iniziali, da ritarare:
    # - sotto ~700 kb/s per MPix → LOW
    # - tra 700 e 1300 → MID
    # - sopra 1300 → HIGH
    if bpp_proxy < 700:
        comp = Complexity.LOW
    elif bpp_proxy < 1300:
        comp = Complexity.MID
    else:
        comp = Complexity.HIGH

    logger.info(
        "[smart] complessità euristica: bpp_proxy≈%.1f kb/s/MPix → %s",
        bpp_proxy, comp.value
    )
    return comp

@dataclass
class SmartPlan:
    complexity: Complexity
    vf_height: int
    v_kbps: int
    floor_kbps: int
    max_video_kbps_by_limit: int
    limit_total_kbps: int
    target_audio_kbps_total: int

def smart_pick_bucket_and_bitrate(
    ctx: EncodeContext,
    limit_bytes: int,
    n_audio_eff: int,
    complexity: Complexity,
) -> SmartPlan:
    """
    Sceglie bucket (vf_height) e v_kbps "smart" rispettando:
    - limite di dimensione (limit_bytes),
    - numero di tracce audio,
    - floors SMART_FLOORS_KBPS[bucket][complexity],
    - MAX_TARGET_HEIGHT,
    - non superare il bitrate video stimato della sorgente.
    """

    if ctx.duration_s <= 0:
        raise ValueError("Durata non valida in smart_pick_bucket_and_bitrate")

    # Stima kb/s consentiti dal limite.
    limit_total_kbps = int((limit_bytes * 8) / ctx.duration_s / 1000.0)
    target_audio_kbps_total = max(0, n_audio_eff * AUDIO_KBPS_PER_TRACK)

    max_video_kbps_by_limit = max(
        MIN_VIDEO_KBPS,
        limit_total_kbps - target_audio_kbps_total
    )

    logger.info(
        "[smart] limite bytes=%s (%d B) → limit_total_kbps≈%d, "
        "audio_kbps_total_target=%d, max_v_by_limit≈%d",
        _fmt_bytes(limit_bytes),
        limit_bytes,
        limit_total_kbps,
        target_audio_kbps_total,
        max_video_kbps_by_limit,
    )

    # Nessun senso aumentare oltre il sorgente.
    max_video_kbps_global = min(max_video_kbps_by_limit, ctx.source_video_kbps_est)

    # ---- Candidati bucket (risoluzioni) ----
    # Niente upscale: non superiamo ctx.height, e rispettiamo MAX_TARGET_HEIGHT.
    max_h = min(ctx.height, MAX_TARGET_HEIGHT)
    candidate_heights = sorted(
        [h for h in SMART_FLOORS_KBPS.keys() if h <= max_h]
    )
    if not candidate_heights:
        # Se il video è addirittura più piccolo del bucket minimo, usiamo il più basso
        # (la tua logica di scale/no-scale si valuterà poi in ffmpeg).
        candidate_heights = [min(SMART_FLOORS_KBPS.keys())]

    logger.info(
        "[smart] candidate buckets (<=%d): %s",
        max_h, ", ".join(str(h) for h in candidate_heights)
    )

    chosen = None

    # Proviamo dal bucket più alto verso il basso.
    for h in sorted(candidate_heights, reverse=True):
        floors = SMART_FLOORS_KBPS[h]
        floor_kbps = floors[complexity.value]

        # v_kbps massimo che possiamo permetterci su questo bucket.
        v_kbps_candidate = max_video_kbps_global

        logger.info(
            "[smart] bucket %d: floor=%d kb/s (complexity=%s), "
            "max_v_global=%d kb/s",
            h, floor_kbps, complexity.value, v_kbps_candidate
        )

        if v_kbps_candidate < floor_kbps:
            logger.info(
                "[smart] bucket %d scartato: v_kbps_candidate=%d < floor=%d",
                h, v_kbps_candidate, floor_kbps
            )
            continue

        # Se arriviamo qui, questo bucket è fattibile.
        chosen = SmartPlan(
            complexity=complexity,
            vf_height=h,
            v_kbps=v_kbps_candidate,
            floor_kbps=floor_kbps,
            max_video_kbps_by_limit=max_video_kbps_by_limit,
            limit_total_kbps=limit_total_kbps,
            target_audio_kbps_total=target_audio_kbps_total,
        )
        logger.info(
            "[smart] bucket %d fattibile: scelgo v_kbps=%d (≥ floor=%d).",
            h, v_kbps_candidate, floor_kbps
        )
        break

    if chosen is None:
        # Nessun bucket compatibile col limite → al chiamante la decisione di split.
        msg = (
            "Nessun bucket compatibile con il limite: "
            f"max_video_kbps_global={max_video_kbps_global}, "
            f"min_floor={min(f[complexity.value] for f in SMART_FLOORS_KBPS.values())}"
        )
        logger.info("[smart] %s", msg)
        raise EncodeNotFeasible(msg)

    return chosen

def smart_plan_for_size(
    ctx: EncodeContext,
    limit_bytes: int,
    n_audio_eff: int,
) -> SmartPlan:
    """
    Wrapper: classifica la complessità e poi sceglie bucket+bitrate.
    (Per ora usa solo l'euristica bpp_proxy; in un secondo step si
    potranno aggiungere SI/TI + VMAF e ricalibrare complexity.)
    """
    logger.info(
        "[smart] calcolo piano smart per durata=%s (%.0fs), "
        "limit_bytes=%s (%d B), n_audio_eff=%d",
        _fmt_hms(ctx.duration_s),
        ctx.duration_s,
        _fmt_bytes(limit_bytes),
        limit_bytes,
        n_audio_eff,
    )

    complexity = classify_complexity_heuristic(ctx)
    plan = smart_pick_bucket_and_bitrate(
        ctx=ctx,
        limit_bytes=limit_bytes,
        n_audio_eff=n_audio_eff,
        complexity=complexity,
    )

    logger.info(
        "[smart] piano scelto: complexity=%s, vf_h=%d, v_kbps=%d, "
        "floor=%d, limit_total_kbps=%d, audio_kbps_total=%d",
        plan.complexity.value,
        plan.vf_height,
        plan.v_kbps,
        plan.floor_kbps,
        plan.limit_total_kbps,
        plan.target_audio_kbps_total,
    )
    return plan

def smart_check_file(file_path: str, chat_id: int, item_id: int, app) -> dict:
    """
    Variante 'smart' di check_file che:
    - Se originale <= limite: original_ok (come check_file).
    - Altrimenti:
      - decide quali audio tenere (loggando),
      - estrae EncodeContext,
      - calcola un piano video 'smart' (complessità, bucket, v_kbps),
      - NON lancia encode, ma ritorna il piano.
    Serve per testare/loggare il comportamento prima di integrare l'encode reale.
    """
    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

        # safe_limit_bytes è globale (come nella tua check_file)
        if orig_size <= safe_limit_bytes:
            logger.info(
                "[smart_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,
            }

        # ---- app / profilo lingua (come in check_file) ----
        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
        )

        # ---- Analisi tracce audio + decisione keep/remove ----
        langs = _analyze_audio_languages_with_docker(src)
        filter_enabled, keep_relidx, notify_msg, summary_str = _decide_audio_selection(
            profile_language, langs
        )

        logger.info("[smart_audio] Tracce: %s", summary_str)
        if notify_msg:
            try:
                send_telegram_message(
                    owner_chatid,
                    f"ℹ️ {src.name}: {notify_msg}"
                )
            except Exception:
                pass

        ctx = extract_encode_context(str(src))

        # Log 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(
                "[smart_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(
                "[smart_audio] selection: keep=all (%d/%d), remove=(none)",
                ctx.n_audio,
                ctx.n_audio,
            )

        dur_txt = _fmt_hms(ctx.duration_s)

        # Log sorgente
        try:
            send_telegram_message(
                chat_id,
                "️ [SMART] Analisi compressione GPU (HEVC NVENC)… (solo test piano)"
            )
            a_src_est = max(0, ctx.source_total_kbps_est - ctx.source_video_kbps_est)
            logger.info(
                "[smart_src] name=%s, dur=%s (%.0fs), size=%s (%d B), "
                "limit=%s (%d B), app=%s, profile_lang=%s",
                src.name,
                dur_txt,
                ctx.duration_s,
                _fmt_bytes(orig_size),
                orig_size,
                _fmt_bytes(safe_limit_bytes),
                safe_limit_bytes,
                app,
                profile_language or "n/a",
            )
            logger.info(
                "[smart_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(
                "[smart_src] audio: tracks=%d, a_src_est≈%d kb/s",
                ctx.n_audio,
                a_src_est,
            )
            logger.info(
                "[smart_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

        # ---- Calcolo piano smart (NIENTE ENCODE) ----
        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

        logger.info(
            "[smart_plan] uso limit_try1=%s (%d B, fudge=%.2f%%), n_audio_eff=%d",
            _fmt_bytes(limit_try1),
            limit_try1,
            FUDGE_VBR_FIRST_TRY * 100.0,
            n_audio_eff,
        )

        try:
            smart_plan = smart_plan_for_size(
                ctx=ctx,
                limit_bytes=limit_try1,
                n_audio_eff=n_audio_eff,
            )
        except EncodeNotFeasible as e:
            # Pianificazione non possibile → segnaliamo che servirà split
            logger.info(
                "[smart_plan] Encode non fattibile con i floors smart: %s", e
            )
            try:
                send_telegram_message(
                    chat_id,
                    f"❌ [SMART] Impossibile rispettare i floor minimi con il limite: {e}\n"
                    "Suggerito split/gestione a monte."
                )
            except Exception:
                pass
            return {
                "status": "too_big",
                "path": str(src),
                "size": orig_size,
                "msg": "smart_encode_not_feasible",
            }

        # Riepilogo finale del piano (solo log, nessun encode)
        try:
            send_telegram_message(
                chat_id,
                "ℹ️ [SMART] Piano stimato:\n"
                f"- complessità: {smart_plan.complexity.value}\n"
                f"- vf_height: {smart_plan.vf_height}\n"
                f"- v_kbps: ~{smart_plan.v_kbps} kb/s\n"
                f"- audio totale: ~{smart_plan.target_audio_kbps_total} kb/s\n"
                f"- totale piano: ~{smart_plan.limit_total_kbps} kb/s (limite fudgiato)"
            )
        except Exception:
            pass

        logger.info(
            "[smart_final] piano: complexity=%s, vf_h=%d, v_kbps=%d, "
            "floor=%d, audio_kbps_total=%d, limit_total_kbps≈%d, "
            "safe_limit_bytes=%d (%s)",
            smart_plan.complexity.value,
            smart_plan.vf_height,
            smart_plan.v_kbps,
            smart_plan.floor_kbps,
            smart_plan.target_audio_kbps_total,
            smart_plan.limit_total_kbps,
            safe_limit_bytes,
            _fmt_bytes(safe_limit_bytes),
        )

        # Ritorniamo SOLO il piano (nessun file creato/ritoccato)
        return {
            "status": "smart_plan",
            "path": str(src),
            "size": orig_size,
            "plan": {
                "complexity": smart_plan.complexity.value,
                "vf_height": smart_plan.vf_height,
                "v_kbps": smart_plan.v_kbps,
                "floor_kbps": smart_plan.floor_kbps,
                "audio_kbps_total": smart_plan.target_audio_kbps_total,
                "limit_total_kbps": smart_plan.limit_total_kbps,
            },
        }

    except Exception as e:
        logger.exception("[smart_check_file] eccezione: %s", e)
        try:
            send_telegram_message(chat_id, f"❌ Errore smart_check_file: {e}")
        except Exception:
            pass
        return {"status": "error", "path": None, "msg": str(e)}