Files
obsidian-vault/personal/plans/watchlist-tmdb-missing-marker.md
T

47 KiB
Raw Blame History

Watchlist — автопоиск TMDB + маркер «нет в базе» (без ручных правок)

Дата: 2026-09-22 Статус: ✅ Шаги 1, 1.3, 2, 3 ВЫПОЛНЕНЫ, ЗАДЕПЛОЕНЫ НА КРА́КЕН И ПРИНЯТЫ В UI. Осталось: коммит правок в lampac-scripts, имя в карточке из TMDB/КП, порядок категорий в ленте. Связано: personal/plans/lampac-watchlist-plugin, family/projects/watchlist-automation, family/projects/watchlist-sync, personal/tech/lampac-deployment


✅ ФИНАЛ 2026-09-22: шаги 1, 1.3, 2, 3 закрыты

Коммиты (оба запушены, SHA сверены с remote)

Репо Remote SHA Что
lampac-scripts gitea git_admin/lampac-scripts 1765c05 генератор + деплой + маркер + приёмки
watchlist-sync github mallexxx/watchlist-sync 396acc3 Шаг 3: TMDB-фильтр в discover.py + cli.py

Проверка: git rev-parse HEAD == git ls-remote origin refs/heads/main — совпало обоих.

📊 Результат прогона генератора (сырьё, не оценка)

Метрика Было (задеплоено) Стало
Позиций в каталоге 403 373
Без TMDB-ID 48 0
Автопоиск по названию (2b) — 17
Автопоиск по kp_id (2c) — 4
Нерезолвленных (missing.json) — 30
Источники — Radarr 9, TMDB 343, карточек 373

Разбивка: films 331 (постеров 327) · new 32 · cartoons 3 · anime 7. Скрыто без TMDB: films 29, new 1.

Шаг 1.3 — ПЕРЕДЕЛАН, теперь верен

Было (неверно): mark_missing.py решал «нет TMDB» по тексту .md → 344 пометки вместо 48. Помечал заведомо рабочие позиции («Лило и Стич», «28 лет спустя» — в каталоге они с постерами). В прод не пошёл.

Стало (верно): генератор пишет снимок нерезолвленных → missing.json рядом с JSON каталога ({"generated":true,"count":30,"missing":[...]}), маркер ставится по факту промаха генератора:

python3 scripts/gen_watchlist.py <путь.md> <путь/watchlist.json>   # пишет missing.json
python3 scripts/mark_missing.py --missing <путь>/missing.json <путь.md> [--apply]

Dry-run на реальном файле (.md с Кра́кена, 596 позиций):

Категория Кол-во
всего позиций 596
не в missing.json 350
🗓️ (не тронуты) 39
[x] просмотрено (не тронуты) 179
уже с ❓ 10
будет помечено 18

Помеченные — черновые заметки с опечатками и подсказками в скобках, ровно то, что требует ручной правки названия: Джеймс Кемерон морские приключения?, Разговорник/Квартетник, Аэроплан (Нильсен), Солярис Тарковский, Идиот Бортко, Спрут сериал, бердманн, Hemlock Grove (сериал), Pandas 2018/Doc, Some like it hot 1959 в джазе только деву, Мулан фильм, Москва слезами не верить, Байкал удивительное приключение юмы, Перед почтением сжечь, Курасава скрытая крепость, Клетка (джей ло), Ирана, Лена 2012.

⚠️ Сопоставление — по нормализованному названию (нижний регистр, ё→е, без пунктуации/эмодзи, без хвостового года), устойчиво к правкам .md ночным кроном. Запись атомарная (tmp + rename), бэкап .bak-questions, проверка «файл не стал меньше».

Шаг 3 — ВЫПОЛНЕН, живой dry-run

Файлы: watchlist_sync/discover.py (+_resolvable(), +tmdb_key), cli.py (передаёт TMDB_KEY).

Проверка идёт после фильтра по рейтингу, голосам, году и дедупликации. upcoming (🗓️) не проверяется — календарь информативен, в каталог они не идут.

cd ~/Developer/watchlist-sync && . ./.env && .venv/bin/python -m watchlist_sync.cli discover

Отсеяно 6 позиций (ровно те, что дали бы «здесь пусто»): Одиссея · Майкл · Убежище · Обитель зла · Приглашение · Властелины вселенной

Из 150 released + 50 upcoming прошло: 5 released, 16 upcoming.

Грабли сессии (не повторять)

  1. 🔴 Патчи по якорю не находили место и тихо пропускались: импорт SequenceMatcher, константы KP_BASE/UA/KP_SIM_THRESHOLD, блок stats в enrich(), блок 2b, отчёт main(). Сообщения «= … (уже есть)» маскировали промах. Итог: функции вставились, а зависимости — нет; файл падал на NameError. Правило: после патча проверять не только «синтаксис валиден», но и наличие каждой константы/импорта; критичные правки — через patch, а не строковую замену.
  2. 🔴 run() получил tmdb_key в тело, но не в сигнатуру — Pyright сразу показал No parameter named "tmdb_key" в cli.py. Сигнатуру править ОТДЕЛЬНО.
  3. 🔴 Откат из бэкапа вернул более старую версию. cp gen_watchlist_v2.py.bak-step1b … — снимок делался ДО записи, поэтому «бэкап» был старше правок. Правило: сначала снимок, потом правки.
  4. 🔴 Подстановка $( … | sed … ) вырезается и в write_file, и в patch. Строка собирается программно (питфолл №2 из family/how-to/gitea-config.md).

🔴 Прогресс 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).

Осталось (после финала — только 2 пункта)

  1. Деплой нового gen_watchlist.py на Кра́кен (lampac/scripts/) + обновить ~/scripts/watchlist-catalog.sh, чтобы после генерации гонялся mark_missing.py. ⚠️ Проверить, что на Кра́кене есть KP_KEY в /srv/.../watchlist-sync/.env — без него уровни 2b/2c не работают.
  2. Имя в карточке из TMDB/КП, не из .md — требование Alex («Отображать как в tmdb/kp», «Ничего не укорачивать»). Сейчас генератор пишет в карточку it["name"] = название из .md.

Выполнено: шаги 1, 1.3, 2, 3; репо lampac-scripts (gitea 1765c05), watchlist-sync (github 396acc3).


✅ ПОСТ-ДЕПЛОЙНАЯ ИТЕРАЦИЯ (2026-09-22, поздний вечер)

🔴 Генератор НЕ ЧИСТИЛ кэш промахов — исправлено

Alex спросил прямо: «че блядь значит чистка кэша обязательна?! скрипт блядь её делает?!» — и был прав: не делал.

key = f"{title.lower().strip()}|{year or ''}"
if key in cache:
    return tuple(v) if v else None      # промах вернулся, в сеть не пошли

Инвалидации не было. Промах (None) писался навсегда → правка названия, не меняющая ключ кэша (регистр, пробел, порядок слов), ❓ не снимала.

Фикс: clear_failed_cache() — вызывается в main() сразу после load_cache(). Удаляет все None, успешные резолвы (~350) не трогает. Промахи (~50) перезапрашиваются на каждом прогоне.

Залито на Кра́кен, сумма c556c52b8cf4714686add4083c4afc09 (совпала с локальной). Патч: ~/lampac-tmp/patch_autoclear_cache.py, бэкап .bak-cacheclear.

⚠️ Ручная чистка clear_cache_kraken.py больше не нужна — это часть прогона. Подробности: personal/tech/lampac-deployment, раздел «Кэш промахов».

Правки Alex в .md: он делает их САМ, скрипт подхватывает

Alex уточнил три названия — добавил год, снял ❓ и подсказки-подбуллеты:

Было Стало
❓ Everything Everywhere All at Once 2021 + 2 подбуллета Everything Everywhere All at Once 2022
❓ Духless + 2 подбуллета Духless 2012
❓ Бумбараш + 2 подбуллета Бумбараш 1972

🔴 Синк vault — почасовой. Alex правит в Obsidian на Mac, на Кра́кен уезжает через sync-vault.sh (крон 0 * * * *). Дифф .md до синка пустой — прогонять «после правок» нужно ПОСЛЕ синка, иначе генератор увидит старую версию. Правки вручную гоняются: ssh kraken 'bash ~/scripts/sync-vault.sh'.

Ответы на вопросы Alex (проверено crontab -l и кодом)

  1. Порядок в ленте — БАГ, не задумано. watchlist.yaml: displayindex: 1 — самый низкий из всех (rezka 10, kinobase 15, filmix 20, vokino 30, eneyida 35, kinoukr 40, getstv 45, aniliberty 60). «Мой список» должен быть ПЕРВЫМ, но оказался ниже «Сейчас Смотрят» и «Рекомендуем посмотреть». Эти два блока — встроенные каталоги Lampa, не из sites/, поэтому displayindex их не сдвигает. watchlist.yaml в этой сессии НЕ правился. Требует отдельного разбора.
  2. Маркеры — да, так. ❓ = резолв не нашёл, не в каталоге. 🗓️ = не вышел, не в каталоге. Без маркера = вышел и найден → в каталоге.
  3. Прогон после правок сработает — при условии, что синк прошёл и кэш промахов очищен (теперь автоматически).
  4. Radarr и лимит — это РАЗНЫЕ лимиты, не один.
    • batch=2 (снижен с 5 после инцидента 2026-09-21, диск 100%) — сколько позиций уходит в Radarr на закачку за прогон. Плюс _free_gb() — стоп при нехватке места.
    • Выдача в Lampa лимита НЕ имеет — отдаётся весь watchlist.json.
    • Каталог строится из .md → watchlist.json; Radarr в отображении не участвует.
    • «Один скрипт в кроне» — неверно, их пять (см. таблицу ниже).

Осталось

  1. Коммит в lampac-scripts — на gitea лежит 1765c05; не закоммичены: фикс .env-пути, clear_failed_cache(), переделанный mark_missing.py. Это единственный реально висящий технический долг.
  2. Имя в карточке из TMDB/КП — «отображать как в tmdb/kp, ничего не укорачивать». Сейчас генератор пишет it["name"] = название из .md.
  3. Порядок категорий в ленте — разобраться со встроенными блоками Lampa.
  4. Доки — обновлены (этот файл + personal/tech/lampac-deployment).

✅ ДЕПЛОЙ НА КРА́КЕН ВЫПОЛНЕН (2026-09-22, вечер)

🔴🔴 Главная находка деплоя: генератор на Кра́кене НИКОГДА не резолвил

load_env() искал .env только по Mac-пути:

for p in (Path("/Users/admin/Developer/watchlist-sync/.env"),):

На Кра́кене такого пути нет → TMDB_KEY не найден → [ERR] нет TMDB_KEY в .env → каталог собирался на одних Кинопоиск-ссылках, без TMDB-ID и постеров.

Это объясняет, почему до сессии в JSON не было TMDB-ID у половины позиций: дело было не в слабом резолве, а в том, что резолв вообще не запускался.

Фикс: load_env() перебирает 5 мест (последний найденный по ключу побеждает), печатает, какие файлы прочитаны:

candidates = [
    os.environ.get("WATCHLIST_ENV", ""),
    "/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync/.env",
    "/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/watchlist-sync/.env",
    str(Path.home() / "Developer" / "watchlist-sync" / ".env"),
    str(Path.home() / ".config" / "watchlist-sync" / ".env"),
]

Скрипт патча: ~/lampac-tmp/patch_envpath.py. Бэкап: .bak-envpath.

Боевой прогон на реальном .md (не тест)

ssh kraken "cd $H && python3 $H/scripts/gen_watchlist.py $W $H/wwwroot/watchlist.json"
Метрика Значение
позиций в каталоге 373
без TMDB-ID 0
films / new / cartoons / anime 331 / 32 / 3 / 7
постеров 327 / 32 / 3 / 7
missing.json 30
источники Radarr 9, TMDB 343, автопоиск 2b: 17, 2c: 4, miss 30

Размеры: watchlist.json 102 456 Б, missing.json 1 239 Б.

Что задеплоено на Кра́кен

Что Путь Права Сумма
генератор $H/scripts/gen_watchlist.py kraken:kraken 600 3c7d4df3…
маркер $H/scripts/mark_missing.py kraken:kraken 600 43b3bb15…
крон-скрипт ~/scripts/watchlist-catalog.sh root:root 755 17672d62…
бэкап генератора $H/scripts/gen_watchlist.py.bak-20260922-175754

$H = /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac

Суммы сверены md5 локально и на Кра́кене — совпали. py_compile + bash -n — OK.

🔴 Права критичны: установка только через sudo install -o kraken -g kraken -m 600 — каталог $H/scripts/ принадлежит kraken:kraken, но ~/scripts/ — root:root 755. cp без sudo падает на Permission denied (грабля: бэкап крона-скрипта из-за этого не сделался с первого раза).

watchlist-catalog.sh — обновлён

Добавлен блок маркера между генерацией и перезапуском lampac:

python3 "$GEN" "$WATCHLIST" "$OUT" 2>&1 | tail -12 || { log "ERR: генерация упала"; exit 1; }

if [ -f "$MISSING" ] && [ -f "$MARK" ]; then
    log "Простановка ❓ по missing.json"
    python3 "$MARK" --missing "$MISSING" "$WATCHLIST" --apply 2>&1 | tail -8 \
        || log "WARN: маркер не проставился (не фатально)"
fi

docker restart lampac   # сброс кэша Catalog

MISSING=$LAMPAC/wwwroot/missing.json. Нефатально для маркера — генерация важнее.

Крон на Кра́кене — полная картина (проверено crontab -l)

0 1  * * *   watchlist-nightly.sh     process-thumbs → resolve → sync-down → sync-up
0 9  * * 0   watchlist-discover.sh    новинки из KP (воскресенье)
0 2  * * *   watchlist-cleanup.sh     удаление просмотренного при диске >90%
30 2 * * *   watchlist-catalog.sh     JSON-каталог для Lampa ← ОБНОВЛЁН
0 *  * * *   sync-vault.sh            синк vault
0 *  * * *   pgs-to-srt.sh            (новая загрузка) — не watchlist
0 3  * * *   pgs-to-srt.sh            (вся библиотека) — не watchlist

⚠️ «Один скрипт в кроне» — неверно, их пять по watchlist. Разделение ролей: discover только пишет в .md, в закачку не отправляет. Закачку отправляет sync-down через Radarr (radarr_add() + radarr_search(), sync_down.py:155,158). Лимит есть: batch снижен 5 → 2 после инцидента 2026-09-21 (диск 100%), плюс проверка свободного места _free_gb().

Осталось открытым

  1. Коммит в lampac-scripts — mark_missing.py (переделка) и фикс .env-пути ещё не закоммичены. В gitea лежит 1765c05, там mark_missing.py старой версии.
  2. Имя в карточке из TMDB/КП — требование Alex «отображать как в tmdb/kp, ничего не укорачивать». Сейчас генератор пишет it["name"] = название из .md.
  3. Порядок в ленте Lampa — «Мой список» (displayindex: 1, самый низкий из всех) оказался ниже встроенных блоков «Сейчас Смотрят» и «Рекомендуем посмотреть». Встроенные каталоги Lampa не из sites/, поэтому displayindex их не сдвигает. Требует отдельного разбора — правился ли watchlist.yaml в этой сессии: НЕТ.

Приёмка Alex (частично)

Alex подтвердил: «вижу фильмы в ленте, работают» — то есть каталог в UI виден и играет. Это закрывает приёмку пунктов 1–3 из плана. Осталось: имя в карточке и порядок категорий.

Порядок резолва — финальная реализация в коде

# 1) Radarr по названию
# 2) TMDB по названию как есть
# 2c) kp_id_from_url(it["kp_href"]) -> kp_search_by_id() -> _docs_to_tmdb()
# 2b) tmdb_via_kp() с KP_SIM_THRESHOLD
# 3) miss -> missing.json

Функции: kp_id_from_url(), kp_search_by_id(), _docs_to_tmdb(), tmdb_via_kp_id(), tmdb_via_kp(), kp_search(), title_sim(). Константы: KP_BASE, UA, KP_SIM_THRESHOLD = 0.75, KP_ID_IN_URL.

_docs_to_tmdb() — общая логика для 2b и 2c. Параметр _by_id на документе отключает проверку порога: если фильм пришёл по kp_id, близость названия не важна — id и есть доказательство.


Требование 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 через Кинопоиск:

# 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 из резолвера. Новая функция:

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 1765c05). Пишет watchlist.json + cards/ + missing.json
~/Developer/lampac-scripts/scripts/deploy_lampac.py заливка на Кра́кен
~/Developer/lampac-scripts/scripts/mark_missing.py маркер ❓ по missing.json (переделан, --missing)
~/Developer/lampac-scripts/tests/accept_step1*.py приёмки автопоиска (3/3)
~/lampac-tmp/ рабочая свалка + все патч-скрипты сессии (НЕ git)
Кра́кен: lampac/scripts/gen_watchlist.py ЗАДЕПЛОЕНА НОВАЯ ВЕРСИЯ (3c7d4df3…, kraken:kraken 600), бэкап старой .bak-20260922-175754
Кра́кен: lampac/scripts/mark_missing.py задеплоен (43b3bb15…), ставит ❓ по missing.json
Кра́кен: ~/scripts/watchlist-catalog.sh обновлён (17672d62…) — генерация → маркер → restart lampac
Кра́кен: lampac/wwwroot/missing.json снимок нерезолвленных (30 позиций)
~/lampac-tmp/patch_envpath.py фикс load_env() — путь .env для Кра́кена (КРИТИЧНО)
~/Developer/watchlist-sync/watchlist_sync/resolver.py ЭТАЛОН cross-reference
~/Developer/watchlist-sync/watchlist_sync/discover.py TMDB-фильтр (Шаг 3, _resolvable())
~/Developer/watchlist-sync/watchlist_sync/cli.py discover передаёт TMDB_KEY
~/.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.