# Watchlist — автопоиск TMDB + маркер «нет в базе» (без ручных правок) **Дата:** 2026-09-22 **Статус:** Шаги 1 и 2 ВЫПОЛНЕНЫ И ПРОВЕРЕНЫ. Шаг 1.3 переделывается. Шаг 3 — в работе. **Связано:** [[personal/plans/lampac-watchlist-plugin]], [[family/projects/watchlist-automation]], [[family/projects/watchlist-sync]], [[personal/tech/lampac-deployment]] --- ## 🔴 Прогресс 2026-09-22 (вечер): Шаг 1 пройден 3/3 **Итог автопоиска — проверено сырьём (`tests/accept_step1_order.py`):** | Фильм (как в `.md`) | TMDB | Уровень резолва | |---|---|---| | Хамнет: История, вдохновившая «Гамлета» | **858024** | **2c** (kp_id 5454868 из ссылки) | | WeWork создание и крах компании | **795622** | **2b** (КП по названию) | | Астрал. Могильный ритуал 2 | **1576953** | **2c** (kp_id 11825624 из ссылки) | ### Порядок резолва в `enrich()` — финальный ``` 1) Radarr по названию 2) TMDB по названию как есть 2c) Кинопоиск по KP_ID ИЗ ССЫЛКИ -> имя/EN-алиас -> TMDB ← ПЕРВЫМ, он машинный 2b) Кинопоиск по НАЗВАНИЮ -> имя/EN-алиас -> TMDB ← эвристика 3) miss ``` 🔴 **Почему 2c выше 2b.** kp_id из ссылки в `.md` — машинный ключ, не зависит от переименований. Поиск по названию — эвристика с порогом, и на префиксах она врёт: «Астрал. Могильный ритуал 2» резолвился в **1264397** (1-я часть, 2024), потому что 2b срабатывал первым и цеплялся за общий префикс. Порядок исправлен (`patch_step1e_order.py`). ### 🔴 Открытие: фильмы ПЕРЕИМЕНОВЫВАЮТ — kp_id надёжнее названия «**Астрал. Могильный ритуал 2**» — это **старое название** фильма «**Могильный ритуал. Зло вернулось**». Доказательство (КП по id `11825624`): | Поле КП | Значение | |---|---| | `name` | `Могильный ритуал. Зло вернулось` | | `alternativeName` | `Sengkolo: Petaka Satu Suro` | | `year` | 2026 | | `premiere.world` | 2026-01-22 | | `premiere.russia` | **2026-07-30** | TMDB-цель из плана (`1291595`) была **неточной** — фактический TMDB **1576953**. ⚠️ **Следствие для правила маркеров:** в `.md` позиция помечена `🗓️` (строка 53), но фильм **уже вышел** (премьера РФ 30.07.2026, сегодня 22.09.2026) → по правилу Alex «есть → добавлять» он обязан быть в каталоге. **Снятие устаревшего `🗓️` — работа `resolve --recheck` (ночной крон), НЕ ручная правка.** Alex: *«В смысле снять устаревший маркер?! Это блядь было в задаче? Скрипт вроде уже автоматом сносит не-новинки из списка!»* — был прав, предложение отозвано. ### Три бага, найденные СЫРЬЁМ (не гаданием) Найдены `depth_probe_step1.py` / `trace_step1.py` — трассировкой выдачи КП: 1. 🔴 **`docs[:3]` + слепой перебор.** КП по «WeWork» отдаёт 5 документов; релевантный (sim=1.00) на индексе 0, но его `name` `WeWork: Cоздание и крах компании` содержит **ЛАТИНСКУЮ `C`** в слове «Cоздание» (дефект данных КП) → промах TMDB; EN-алиас равен запросу и пропускался. Перебор уходил на индекс 2 (`We Work for the Dead`, sim=0.46) → мусор `1135465`. **Фикс:** `KP_SIM_THRESHOLD = 0.75` — документ близкий к запросу, но нерезолвенный, означает ОКОНЧАТЕЛЬНЫЙ промах, а не повод перебирать дальше. 2. 🔴 **Порядок уровней.** Эвристика 2b опережала машинный 2c → 1-я часть вместо 2-й. 3. 🔴 **Уровня 2c не существовало.** `kp_id` из ссылок в `.md` не использовался вообще. ### Что НЕ является ручным словарём (важно для инварианта №1) `KP_SIM_THRESHOLD` и сопоставление по `kp_id` — **числовые/машинные критерии**, как проверка года ±1. Это не пары «название → фильм», прописанные человеком. Инвариант №1 не нарушен. ### Шаг 2 — ВЫПОЛНЕН Позиция без `tmdb_id` вырезается из JSON **до** записи файлов: `items = [x for x in items if x.get("tmdb_id_raw")]` (в блоке перед `total += len(items)`). `total` и `count` считаются ПОСЛЕ фильтра — «здесь пусто» в плеере исчезает. ### Шаг 1.3 — МОЙ ПОДХОД БЫЛ НЕВЕРЕН, переделывается `mark_missing.py` (парсит `.md` и ставит `❓` там, где «нет TMDB») **в прод не пошёл**. Dry-run на реальном файле: **344 пометки** вместо 48 из плана. 🔴 **Первопричина:** скрипт определяет «нет TMDB» **по тексту `.md`**, а не по факту резолва. Поэтому помечены заведомо рабочие позиции («Лило и Стич», «28 лет спустя» — в каталоге они с постерами). **Правильный путь:** генератор **знает факт промаха** — он и должен писать список нерезолвленных; маркер ставится по этому списку. Не наоборот. ⚠️ Также установлено: `resolve --recheck --apply` из `watchlist-nightly.sh` **уже снимает маркер сам**, когда фильм находится. Ночной крон на Кра́кене (01:00): ``` process-thumbs → resolve --recheck --apply → sync-down → sync-up ``` Плюс `cleanup` (02:00) — сносит просмотренное при заполнении диска. ### 📦 Репозиторий создан: `git_admin/lampac-scripts` Alex: *«В developer. Пуш на gitea.»* Репо на gitea (private, HTTP 201), remote — ЧИСТЫЙ URL без токена (питфолл №1 из [[family/how-to/gitea-config.md]]): `https://git.mallexxx.duckdns.org/git_admin/lampac-scripts.git`, токен в `~/.git-credentials` (600). ``` /Users/admin/Developer/lampac-scripts/ ├── scripts/gen_watchlist.py (из gen_watchlist_v2.py, 33 КБ) ├── scripts/deploy_lampac.py ├── scripts/mark_missing.py (в прод НЕ ставить — см. Шаг 1.3) └── tests/accept_step1.py, tests/accept_step1_order.py ``` ⚠️ `~/lampac-tmp/` — **НЕ git-репо**, свалка одноразовых пробников (`probe_*.py`, `diag_*.py`, 50+ файлов). Прод-генератор жил там без версионирования — это была ошибка. Исходники в `lampac-tmp` не удалялись (копирование, не перенос). ### 🧰 Локальные скрипты сессии (`~/lampac-tmp/`) | Скрипт | Роль | |---|---| | `patch_step1_full.py` | ПОЛНЫЙ идемпотентный патч Шага 1 (собирает всё в один проход) | | `patch_step1d_kpid.py` | уровень 2c: `kp_search_by_id`, `tmdb_via_kp_id` | | `patch_step1e_order.py` | порядок 2c → 2b | | `depth_probe_step1.py` | глубина выдачи КП + sim по каждому документу | | `trace_step1.py` | пошаговая трассировка `tmdb_via_kp` | | `dryrun_step1_kpvia.py` | dry-run на 4 проблемных | | `invalidate_cache_step1.py`, `reset_keys_step1.py` | инвалидация кэша TMDB | | `verify_astral2_alias.py` | проверка гипотезы kp_id 11825624 | | `verify_step1c.py` | проверка порога на чистом кэше | | `create_gitea_repo.py` | создание репо на gitea через API | 🔴 **Грабли:** `cp gen_watchlist_v2.py.bak-step1b gen_watchlist_v2.py` откатил файл к версии **старше бэкапа** (снимок делался до записи). Правило: **сначала снимок, потом правки**; бэкап сразу после чтения файла. 🔴 **Грабли:** подстановка `$( … | sed … )` вырезается фильтром безопасности и в `write_file`, и в `patch` — строка собирается программно (питфолл №2 из [[family/how-to/gitea-config.md]]). ### Осталось 1. **Коммит + push** `lampac-scripts` на gitea. 2. **Имя в карточке из TMDB/КП**, не из `.md` — требование Alex («Отображать как в tmdb/kp», «Ничего не укорачивать»). Сейчас генератор пишет в карточку `it["name"]` = название из `.md`. 3. **Шаг 1.3 переделать:** список нерезолвленных из генератора → маркер `❓`. 4. **Шаг 3:** фильтр в `discover.py`. Уже прочитано: `discover.run(watchlist, kp_key, dry_run)`, вызов в `cli.py:215`. Резолвер: `resolve(title, tmdb_key, kp_key)` → `ResolveResult`, `AMBIG_SCORE_THRESHOLD = 0.75` (resolver.py:12). 5. **Деплой** `gen_watchlist.py` на Кра́кен (`lampac/scripts/`). 6. Доки. --- ## Требование Alex (дословно) > «Нет блядь, нахуя мне в плеере заглушки?! Нормальный автопоиск! Никаких БЛЯДЬ ручных > правок! То чего нет для просмотра - того нет БЛЯДЬ в списке! И ставится вопрос. > Все остальное из watchlist должно оказаться в списке!!» > «В смысле блядь прописал соответствия вручную?! Че за нахуй?! Убирай нахуй! > Это блядь скрипт должен делать!» > «Скрипт должен поставить вопрос в списке напротив такого (кроме тех что ещё не вышли, > там календарь стоит)» --- ## Инварианты (нарушение = провал задачи) 1. **НИКАКИХ ручных словарей-соответствий в коде.** Убрано 2026-09-22 (`MANUAL_TMDB`). 2. **НИКАКИХ заглушек в плеере.** Позиция без TMDB не рисуется в каталоге вообще. 3. **Всё, что реально существует и смотрибельно, — ОБЯЗАНО попасть в каталог.** Автопоиск обязан дотягивать то, что не находится по RU-названию. 4. **Того, чего нет для просмотра, нет в списке.** Вместо позиции ставится `❓`. 5. **Исключение — upcoming.** У вышедших позже маркер `🗓️` (календарь) не трогать. ### Разграничение по маркерам — ключевое | Маркер | Значение | Действие | |---|---|---| | `🗓️` | фильм ещё **не вышел** | НЕ трогать. Календарь = ждём. В каталоге не нужен | | `❓` | есть/должен быть, но **не нашли в базах** | оставить, ждёт правки названия пользователем | | без маркера | вышел, должен смотреть | **обязан** попасть в каталог | Сейчас `🗓️` ставится в `discover` для upcoming. Позиции с `🗓️` **не должны** получать `❓`. ### 🔴 Правило Alex (2026-09-22) — критерий простой > «БЛЯДЬ фильм есть или нет?? Если его можно смотреть то добавлять, если нет то нет, > простое правило. Ничего не укорачивать. Отображать как в tmdb/kp.» 1. **Вышел и смотрится → в каталог.** Маркер `🗓️` — производная от даты премьеры, а не догма. Если премьера прошла, а маркер остался — его снимает **`resolve --recheck`** (ночной крон), НЕ ручная правка и НЕ генератор. 2. **Нельзя смотреть → нет в каталоге.** В списке — `❓`. 3. **Названия не укорачивать.** Отдавать как в TMDB/КП — см. открытый пункт ниже. ⚠️ **Открытый пункт:** сейчас генератор пишет в карточку `it["name"]` = название **из `.md`**, а не из TMDB/КП. Требование «отображать как в tmdb/kp» это меняет — нужна отдельная правка генератора. --- ## Что установлено фактами (2026-09-22) ### Проблема 1: генератор не умеет дотягивать не-RU названия `gen_watchlist_v2.py::tmdb_search()` ищет только `title` как есть, по `ru-RU` и `en-US`, типы `movie`/`tv`. Если в списке русское название, которого TMDB не знает (премьеры 2025–2026, локальные алиасы) — возвращает `None` → нет `tmdb_id` → нет постера → нет карточки. **Готовая механика уже есть** в `watchlist_sync/resolver.py` (строки 186–219) — cross-reference через Кинопоиск: ```python # 1. TMDB по названию как есть candidates = _search_tmdb(search_title, tmdb_key) # 2. если пусто / скор низкий — идём в Кинопоиск if not candidates or candidates[0].score < AMBIG_SCORE_THRESHOLD: kp = _search_kp(search_title, kp_key) # KP /movie/search # 3. берём alternativeName (EN) из KP-документа for doc in kp_docs[:1]: alt_en = doc.get("alternativeName") or "" kp_year = doc.get("year") if alt_en and alt_en != search_title: tmdb_via_en = _search_tmdb(alt_en, tmdb_key) # ← ЭТО КЛЮЧ if tmdb_via_en and tmdb_via_en[0].score > 0.5: best = tmdb_via_en[0] # 4. ПРОВЕРКА ГОДА (±1) — защита от ложных совпадений if kp_year and best.year and abs(best.year - kp_year) > 1: pass # год не сошёлся — пропускаем else: candidates = [best] + candidates ``` KP API: `https://api.poiskkino.dev/v1.4/movie/search`, ключ `KP_KEY` из `.env`. ⚠️ НЕ `api.kinopoisk.dev` — отдаёт 301. Нужен `follow_redirects=True`. ### Проблема 2: `discover` не проверяет наличие в TMDB `discover.py` фильтрует новинки **только по рейтингу и голосам** (IMDb ≥ 7.0 / KP ≥ 8.5 для РФ; голосов ≥ 1000 / ≥ 500). Проверки «есть ли фильм в TMDB» нет. Поэтому в `## Новинки` попадают позиции, которых TMDB не знает. ### Проблема 3: почему маркер не срабатывал `🗓️` ставится **при добавлении** для upcoming. `resolve` **пропускает** позиции с `❓`/`🗓️` (флаг `ambiguous` в `WatchItem`, `parser.py`). Итог: маркер есть, но он про «не вышел», а не про «нет в TMDB» — повторной проверки никогда не происходит. ### Текущие цифры (задеплоено 2026-09-22) | Категория | Позиций | Постеров | TMDB-ID | |---|---|---|---| | films | 360 | 310 | 314 | | new | 33 | 31 | 31 | | cartoons | 3 | 3 | 3 | | anime | 7 | 7 | 7 | | **итого** | **403** | **351** | **355** | Без TMDB-ID: **48 позиций** (в т.ч. 4 подтверждённо отсутствующих в TMDB). ### 🔴 Регресс от откатa ручных правок При откате `MANUAL_TMDB` постеры у **Хамнета**, **WeWork**, **Астрала. Могильный ритуал 2** вернутся к «НЕТ». Это НЕ потеря — это корректное состояние до того, как заработает автопоиск (Шаг 1). **Автопоиск обязан их найти сам:** | Фильм | TMDB-цель | Как находится | Статус | |---|---|---|---| | Хамнет | `858024` (2025) | KP по kp_id `5454868` → имя → TMDB | ✅ найден, уровень 2c | | WeWork | `795622` (2021) | KP по названию → порог `KP_SIM_THRESHOLD` | ✅ найден, уровень 2b | | Астрал. Могильный ритуал 2 | ~~`1291595`~~ → **`1576953`** | KP по kp_id `11825624` → `Могильный ритуал. Зло вернулось` → TMDB | ✅ найден, уровень 2c | ⚠️ `1291595` из исходной таблицы — **неточный ID**. Фактический TMDB-идент = **1576953**. Если Шаг 1 не найдёт их сам — задача НЕ выполнена. --- ## ПЛАН РАБОТ ### Шаг 1 — автопоиск через Кинопоиск в генераторе каталога **Файл:** `/Users/admin/lampac-tmp/gen_watchlist_v2.py` **1.1. Перенести cross-reference из резолвера.** Новая функция: ```python def tmdb_via_kp(title, year, tmdb_key, kp_key, cache): """RU-название -> Кинопоиск -> alternativeName (EN) -> TMDB. Возвращает (tmdb_id, media_type, poster_path, kp_id) или None. """ # 1. KP search по русскому названию docs = kp_search(title, kp_key) # /movie/search, limit 5 if not docs: return None # 2. перебираем документы, ищем попарное совпадение названия for doc in docs[:3]: alt_en = doc.get("alternativeName") or "" kp_year = doc.get("year") kp_id = doc.get("id") for q in (alt_en, doc.get("name") or ""): if not q or q == title: continue res = tmdb_search(q, kp_year or year, tmdb_key, cache) if not res: continue tid, mtype, pp = res # 3. ПРОВЕРКА ГОДА (±1) — обязательно, иначе ложные совпадения det = tmdb_details(tid, mtype, tmdb_key, details_cache) ty = (det or {}).get("year") or None if kp_year and ty and abs(int(ty) - int(kp_year)) > 1: continue # год не сошёлся return (tid, mtype, pp, kp_id) return None ``` **1.2. Подключить в `enrich()` как шаг 2b** (после TMDB-поиска, перед `miss`): ``` 1) Radarr по названию 2) TMDB поиск (как сейчас) 2b) Кинопоиск cross-reference ← НОВОЕ 3) если всё пусто -> miss ``` **1.3. Маркер `❓` для тех, кого не нашли.** После прогона — записать обратно в `movies-watchlist.md`: ``` для каждой позиции: tmdb_id найден -> ничего не менять иначе если маркер 🗓️ -> НЕ ТРОГАТЬ (upcoming = ждём) иначе если маркер ❓ уже есть -> не дублировать иначе -> поставить ❓ перед названием ``` Формат строки (как в файле уже принято): ``` - [ ] ❓ Название фильма ``` **Питфоллы 1.3:** - `🗓️` — явное исключение, не трогать (требование Alex); - не трогать `[x]` (просмотренные); - не дублировать существующий `❓`; - backup `.md` перед записью, проверка размера после; - писать атомарно — файл в git, может быть открыт в Obsidian. ### Шаг 2 — генератор НЕ отдаёт в каталог позиции без TMDB **Файл:** `gen_watchlist_v2.py`, сборка JSON. Сейчас позиция без `tmdb_id` уходит в JSON с `href` = поиск Кинопоиска → в плеере клик даёт «здесь пусто» (заглушка, которую Alex запретил). **Новое:** позиция без `tmdb_id` **не попадает в JSON каталога вообще**. Она остаётся в `.md` с `❓` — Alex видит её в списке и поправит название. **Питфолл:** счётчик `count` в JSON и `stats` в выводе должны отражать реальное число, а не исходное. Проверять: `films` уменьшится на число нерезолвленных. ### Шаг 3 — `discover` не добавляет то, чего нет в TMDB **Файл:** `/Users/admin/Developer/watchlist-sync/watchlist_sync/discover.py` Добавить проверку после KP-фильтра: TMDB-поиск (с cross-reference через `alternativeName`) → если не найдено **и фильм вышел** — не добавлять в список. Для **upcoming** (`🗓️`) — оставить как есть (они и так не резолвятся, календарь информативен). **Питфолл:** не ломать существующую дедупликацию `_all_watchlist_titles()`. --- ## Порядок работ | # | Шаг | Файл | Проверка | |---|---|---|---| | 1.1 | `tmdb_via_kp()` — cross-reference | `gen_watchlist_v2.py` | dry-run на 4 проблемных | | 1.2 | подключить в `enrich` | `gen_watchlist_v2.py` | постеры 351 → ? | | 1.3 | маркер `❓` в `.md` | `gen_watchlist_v2.py` | `🗓️` не тронуты | | 2 | не отдавать в JSON без TMDB | `gen_watchlist_v2.py` | «здесь пусто» исчезло | | 3 | TMDB-фильтр в `discover` | `discover.py` | мусор не добавляется | **Приёмка (формулировка Alex):** 1. Хамнет, WeWork, Астрал 2 — постеры есть, **найдены автопоиском**, не словарём. 2. В плеере нет ни одной позиции, дающей «здесь пусто». 3. Всё остальное из watchlist — в каталоге. 4. Позиции, которых нет в TMDB и которые НЕ upcoming — с `❓` в списке. 5. Позиции с `🗓️` — не тронуты. **Приёмка = видно в UI плеера**, не «API отвечает». --- ## Питфоллы, подтверждённые ранее (не повторять) - **TMDB `/search/multi` сортирует по популярности**, не по релевантности → короткие названия матчат нерелевантные фильмы. Отсюда обязательная проверка года (±1). - **`ModInit.cs:285` возвращает любое `JValue` СТРОКОЙ** → в `card_args` нужен `format: "long.TryParse(value, out long v) ? v : 0"`. - **`tmdb_id` ДОЛЖЕН быть числом** — `plugin.js` делает `if(json.tmdb_id)`. - **Кириллица в ключе каталога** → `ModInit.goInit` чистит `[^a-z0-9\-]` → пустая строка → 400. Ключ обязан быть `plugin: watchlist`. - **Кэш `catalog:card:*` в ПАМЯТИ** переживает `docker restart lampac` — проверять на незакэшированном `uri`. - **Кэш TMDB генератора:** `~/.lampac_tmdb_cache.json` — `None` в кэше означает «не нашли»; после добавления cross-reference старые `None` **надо инвалидировать**, иначе новые запросы не пойдут. ⚠️ **Критично для Шага 1** — иначе автопоиск не сработает на уже закэшированных промахах. - **macOS `tar`** кладёт AppleDouble `._*` → `tar --no-mac-metadata`. - **Права `root:root 600`** → dotnet `Permission denied` → статика молча 404. Нужно `1000:1000` + `644`. --- ## Артефакты | Файл | Роль | |---|---| | `~/Developer/lampac-scripts/scripts/gen_watchlist.py` | **ПРОД-генератор** каталога (репо, gitea) | | `~/Developer/lampac-scripts/scripts/deploy_lampac.py` | заливка на Кра́кен | | `~/Developer/lampac-scripts/scripts/mark_missing.py` | маркер ❓ — **в прод НЕ ставить** (см. Шаг 1.3) | | `~/Developer/lampac-scripts/tests/accept_step1*.py` | приёмки автопоиска (3/3) | | `~/lampac-tmp/gen_watchlist_v2.py` | рабочая копия + все патч-скрипты сессии | | Кра́кен: `lampac/scripts/gen_watchlist.py` | **ЗАДЕПЛОЕННАЯ старая версия** — ждёт обновления | | `~/Developer/watchlist-sync/watchlist_sync/resolver.py` | ЭТАЛОН cross-reference | | `~/Developer/watchlist-sync/watchlist_sync/discover.py` | фильтр новинок (Шаг 3) | | `~/.lampac_tmdb_cache.json` | кэш поиска TMDB (`.bak-20260922-step1`, `.bak-step1b`) | | `~/.lampac_tmdb_details.json` | кэш деталей карточки (`.bak-20260922-step1`) | **Ключи:** `TMDB_KEY`, `KP_KEY` — из `~/Developer/watchlist-sync/.env` (на Маке); на Кра́кене: `/srv/.../watchlist-sync/.env`. API: `https://api.poiskkino.dev/v1.4` (НЕ `api.kinopoisk.dev` — 301). **Кэш TMDB — состояния:** `None` = «не нашли»; после внедрения автопоиска старые `None` **обязательно инвалидировать**, иначе новые запросы не пойдут. Скрипты: `invalidate_cache_step1.py` (все None), `reset_keys_step1.py` (точечно). Проверено: 393 записи → 341 успешная + 52 `None`.