diff --git a/personal/plans/watchlist-tmdb-missing-marker.md b/personal/plans/watchlist-tmdb-missing-marker.md index 8e8cac14..50dff824 100644 --- a/personal/plans/watchlist-tmdb-missing-marker.md +++ b/personal/plans/watchlist-tmdb-missing-marker.md @@ -1,61 +1,96 @@ -# Watchlist — маркер «нет в TMDB» + заглушка карточки +# Watchlist — автопоиск TMDB + маркер «нет в базе» (без ручных правок) **Дата:** 2026-09-22 -**Статус:** черновик, ждёт апрува -**Связано:** [[personal/plans/lampac-watchlist-plugin]], [[family/projects/watchlist-automation]] +**Статус:** ПЛАН УТВЕРЖДЁН Alex — в работе +**Связано:** [[personal/plans/lampac-watchlist-plugin]], [[family/projects/watchlist-automation]], [[family/projects/watchlist-sync]] --- -## Задача (дословно Alex) +## Требование Alex (дословно) -> «Их нет?! Тогда какого хуя они БЛЯДЬ в списке вообще?! Скрипт должен поставить вопрос -> в списке напротив такого (кроме тех что ещё не вышли, там календарь стоит)» +> «Нет блядь, нахуя мне в плеере заглушки?! Нормальный автопоиск! Никаких БЛЯДЬ ручных +> правок! То чего нет для просмотра - того нет БЛЯДЬ в списке! И ставится вопрос. +> Все остальное из watchlist должно оказаться в списке!!» > «В смысле блядь прописал соответствия вручную?! Че за нахуй?! Убирай нахуй! > Это блядь скрипт должен делать!» +> «Скрипт должен поставить вопрос в списке напротив такого (кроме тех что ещё не вышли, +> там календарь стоит)» + --- -## Что установлено фактами +## Инварианты (нарушение = провал задачи) -### 1. Ручные соответствия — УБРАНЫ (2026-09-22) +1. **НИКАКИХ ручных словарей-соответствий в коде.** Убрано 2026-09-22 (`MANUAL_TMDB`). +2. **НИКАКИХ заглушек в плеере.** Позиция без TMDB не рисуется в каталоге вообще. +3. **Всё, что реально существует и смотрибельно, — ОБЯЗАНО попасть в каталог.** + Автопоиск обязан дотягивать то, что не находится по RU-названию. +4. **Того, чего нет для просмотра, нет в списке.** Вместо позиции ставится `❓`. +5. **Исключение — upcoming.** У вышедших позже маркер `🗓️` (календарь) не трогать. -Я вписал в генератор словарь `MANUAL_TMDB` (Хамнет → 858024, WeWork → 795622, -Астрал 2 → 1291595) и «починил» 3 фильма руками. **Откачено по требованию Alex.** -Причина корректная: подмена результатов — не решение, а маскировка проблемы. +### Разграничение по маркерам — ключевое -⚠️ **Побочный эффект отката:** постеры у Хамнета, WeWork и Астрала 2 вернутся к «НЕТ», -пока не будет автоматического механизма. Данные в `/tmp/wl-out` пока содержат ручные -результаты (задеплоено) — при следующем прогоне генератора они исчезнут. +| Маркер | Значение | Действие | +|---|---|---| +| `🗓️` | фильм ещё **не вышел** | НЕ трогать. Календарь = ждём. В каталоге не нужен | +| `❓` | есть/должен быть, но **не нашли в базах** | оставить, ждёт правки названия пользователем | +| без маркера | вышел, должен смотреть | **обязан** попасть в каталог | -### 2. 4 фильма отсутствуют в TMDB физически +Сейчас `🗓️` ставится в `discover` для upcoming. Позиции с `🗓️` **не должны** получать `❓`. -| Фильм | Кинопоиск-ID | Маркер | TMDB | -|---|---|---|---| -| Рассекая волны: экспедиция «Лас Бальсас» (2025) | 12432920 | 🗓️ | нет | -| Цена вершины (2025) | 11888712 | 🗓️ | нет | -| О добре и зле (2025) | 8184798 | 🗓️ | нет | -| Эхопраксия | — | ❓ | id=1650284 есть, но **без постера** | +--- -Проверено запросами: RU-название, EN-алиас, разные годы — пусто. +## Что установлено фактами (2026-09-22) -### 3. Корень: `discover` не проверяет наличие в TMDB +### Проблема 1: генератор не умеет дотягивать не-RU названия -`discover.py` тянет новинки из Кинопоиска и фильтрует **только по рейтингу и голосам** -(IMDb ≥ 7.0 / KP ≥ 8.5 для РФ, голосов ≥ 1000/500). Проверки «есть ли фильм в TMDB» -**нет**. Поэтому в `## Новинки` попадают позиции, которых TMDB не знает → -`resolve` их не может сопоставить → нет `tmdb_id` → нет постера → клик даёт «здесь пусто». +`gen_watchlist_v2.py::tmdb_search()` ищет только `title` как есть, по `ru-RU` и `en-US`, +типы `movie`/`tv`. Если в списке русское название, которого TMDB не знает (премьеры +2025–2026, локальные алиасы) — возвращает `None` → нет `tmdb_id` → нет постера → нет карточки. -### 4. Почему маркер не сработал +**Готовая механика уже есть** в `watchlist_sync/resolver.py` (строки 186–219) — +cross-reference через Кинопоиск: -`🗓️` (upcoming) ставится **при добавлении** в `discover` для ещё не вышедших фильмов. -`resolve` **пропускает** позиции с `❓`/`🗓️` (флаг `ambiguous` в `WatchItem`). -Итог: маркер есть, но он про «не вышел», а не про «нет в TMDB» — и повторно -позиция не проверяется никогда. Для `Эхопраксии` стоит `❓` с прошлых прогонов, -но резолвер её не тронет, пока маркер не снят (`--recheck`). +```python +# 1. TMDB по названию как есть +candidates = _search_tmdb(search_title, tmdb_key) -### 5. Текущие цифры (после деплоя) +# 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 | |---|---|---|---| @@ -65,80 +100,175 @@ | anime | 7 | 7 | 7 | | **итого** | **403** | **351** | **355** | -Без TMDB-ID: **48 позиций**. +Без TMDB-ID: **48 позиций** (в т.ч. 4 подтверждённо отсутствующих в TMDB). + +### 🔴 Регресс от откатa ручных правок + +При откате `MANUAL_TMDB` постеры у **Хамнета**, **WeWork**, **Астрала. Могильный +ритуал 2** вернутся к «НЕТ». Это НЕ потеря — это корректное состояние до того, как +заработает автопоиск (Шаг 1). **Автопоиск обязан их найти сам:** + +| Фильм | TMDB-цель | Как находится | +|---|---|---| +| Хамнет | `858024` «Гамнет» (2025) | KP → `alternativeName` = "Hamnet" → TMDB | +| WeWork | `795622` (2021) | KP → EN-алиас → TMDB | +| Астрал. Могильный ритуал 2 | `1291595` (2026) | KP → EN-алиас → TMDB | + +Если Шаг 1 не найдёт их сам — задача НЕ выполнена. --- -## ПЛАНА РАБОТ (ждёт апрува) +## ПЛАН РАБОТ -### Шаг 1 — маркер `❓` автоматически в генераторе +### Шаг 1 — автопоиск через Кинопоиск в генераторе каталога -**Файл:** `gen_watchlist_v2.py` (на Маке), функция `enrich` + новый writer в watchlist. +**Файл:** `/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) -> НЕ трогать (календарь = ещё не вышел) - иначе -> поставить ❓ в movies-watchlist.md + tmdb_id найден -> ничего не менять + иначе если маркер 🗓️ -> НЕ ТРОГАТЬ (upcoming = ждём) + иначе если маркер ❓ уже есть -> не дублировать + иначе -> поставить ❓ перед названием ``` -Что писать в `.md`: заменить/добавить `❓` перед названием, как это делает `resolve`. -Формат (как уже принято в файле): +Формат строки (как в файле уже принято): ``` - [ ] ❓ Название фильма ``` -Питфоллы: -- не дублировать `❓`, если он уже стоит; -- **не трогать** позиции с `🗓️` — явное исключение по требованию Alex; +**Питфоллы 1.3:** +- `🗓️` — явное исключение, не трогать (требование Alex); - не трогать `[x]` (просмотренные); -- писать атомарно: backup `.md` → запись → проверка размера. +- не дублировать существующий `❓`; +- backup `.md` перед записью, проверка размера после; +- писать атомарно — файл в git, может быть открыт в Obsidian. -### Шаг 2 — заглушка карточки для позиций без TMDB +### Шаг 2 — генератор НЕ отдаёт в каталог позиции без TMDB -Сейчас клик по такому элементу → `cards/card-movie-.json` не существует → -«здесь пусто». Решение: генерировать карточку-заглушку с тем, что есть. +**Файл:** `gen_watchlist_v2.py`, сборка JSON. -Содержимое заглушки: название, год, рейтинг, ссылка на Кинопоиск, пометка -«нет данных TMDB». +Сейчас позиция без `tmdb_id` уходит в JSON с `href` = поиск Кинопоиска → в плеере +клик даёт «здесь пусто» (заглушка, которую Alex запретил). -⚠️ **Пункт под вопросом** — Alex не подтвердил. Возможно, достаточно маркера `❓` -в списке (он сам увидит список и поправит названия). **Уточнить перед реализацией.** +**Новое:** позиция без `tmdb_id` **не попадает в JSON каталога вообще**. +Она остаётся в `.md` с `❓` — Alex видит её в списке и поправит название. + +**Питфолл:** счётчик `count` в JSON и `stats` в выводе должны отражать реальное число, +а не исходное. Проверять: `films` уменьшится на число нерезолвленных. ### Шаг 3 — `discover` не добавляет то, чего нет в TMDB -**Файл:** `discover.py` в `~/Developer/watchlist-sync/`. +**Файл:** `/Users/admin/Developer/watchlist-sync/watchlist_sync/discover.py` -Добавить проверку в `_has_enough_votes()`-цепочку: после KP-фильтра делать -TMDB-поиск и **пропускать** фильм, если TMDB его не знает. Тогда мусор не попадает -в список вообще. +Добавить проверку после KP-фильтра: TMDB-поиск (с cross-reference через +`alternativeName`) → если не найдено **и фильм вышел** — не добавлять в список. -⚠️ **Риск:** фильмы, которых ещё нет в TMDB, но которые реально существуют, будут -потеряны для списка. Для upcoming (`🗓️`) можно оставить — они и так не резолвятся. +Для **upcoming** (`🗓️`) — оставить как есть (они и так не резолвятся, календарь +информативен). -**Уточнить:** применять ли эту проверку к upcoming тоже. +**Питфолл:** не ломать существующую дедупликацию `_all_watchlist_titles()`. --- ## Порядок работ -| # | Шаг | Файл | Апрув | +| # | Шаг | Файл | Проверка | |---|---|---|---| -| 1 | Маркер `❓` в генераторе | `gen_watchlist_v2.py` | ⏳ | -| 2 | Заглушка карточки (?) | `gen_watchlist_v2.py` | ⏳ требует уточнения | -| 3 | Фильтр TMDB в `discover` | `discover.py` | ⏳ | +| 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` | мусор не добавляется | -**Приёмка:** открыть список — напротив фильмов, которых нет в TMDB и которые НЕ -upcoming, стоит `❓`. Фильмы с `🗓️` не тронуты. +**Приёмка (формулировка Alex):** +1. Хамнет, WeWork, Астрал 2 — постеры есть, **найдены автопоиском**, не словарём. +2. В плеере нет ни одной позиции, дающей «здесь пусто». +3. Всё остальное из watchlist — в каталоге. +4. Позиции, которых нет в TMDB и которые НЕ upcoming — с `❓` в списке. +5. Позиции с `🗓️` — не тронуты. + +**Приёмка = видно в UI плеера**, не «API отвечает». --- -## Открытые вопросы (нужен ответ Alex) +## Питфоллы, подтверждённые ранее (не повторять) -1. **Заглушка карточки** — нужна вообще, или достаточно маркера `❓` в списке? -2. **`discover`** — фильтровать по TMDB только для вышедших, или для upcoming тоже? -3. **Откат 3 постеров** — принять откат (Хамнет/WeWork/Астрал без постера, пока нет - автоматики), или сделать автопоиск по альтернативным названиям через Кинопоиск - (`alternativeName`), как уже делается в `watchlist-sync` резолвере? +- **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`. + +--- + +## Артефакты + +| Файл | Роль | +|---|---| +| `/Users/admin/lampac-tmp/gen_watchlist_v2.py` | генератор каталога | +| `/Users/admin/lampac-tmp/deploy_lampac.py` | заливка на Кра́кен | +| `/Users/admin/lampac-tmp/check_fixed2.py` | проверка постера по названию | +| `~/Developer/watchlist-sync/watchlist_sync/resolver.py` | ЭТАЛОН cross-reference | +| `~/Developer/watchlist-sync/watchlist_sync/discover.py` | фильтр новинок | +| `~/.lampac_tmdb_cache.json` | кэш поиска TMDB | +| `~/.lampac_tmdb_details.json` | кэш деталей карточки | + +**Ключи:** `TMDB_KEY`, `KP_KEY` — из `~/Developer/watchlist-sync/.env` (на Маке). +API: `https://api.poiskkino.dev/v1.4/movie/search` (с `follow_redirects=True`).