[2026-09-22] eagle: personal/plans/watchlist-tmdb-missing-marker.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-22 17:13:18 +06:00
1 parent 1d2876df06
commit 42ac730a46
1 file changed
+204 -74
+204 -74
View File
@@ -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-<id>.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`).