Files
obsidian-vault/family/projects/watchlist-sync.md
T

373 lines
23 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
created: '2026-05-18'
updated: '2026-09-20'
status: active
tags:
- kraken
- jellyfin
- radarr
- sonarr
- watchlist
- project
---
# Watchlist Sync
> Цель: автоматическая загрузка непросмотренных фильмов/сериалов из `family/documents/movies-watchlist.md` через Radarr/Sonarr, синхронизация просмотренных обратно из Jellyfin, и AI-curated рекомендации на основе профиля.
Репо: `~/Developer/watchlist-sync` → GitHub `mallexxx/watchlist-sync`
> ℹ️ **Эта заметка — исходный дизайн-документ проекта (Stage 1/2).** Актуальное состояние, рабочие пути, cron-статус и все найденные баги — в [[family/projects/watchlist-automation]]. Реализовано всё, кроме Stage 2 (рекомендации). Ниже сохранён дизайн; расхождения с реальностью помечены.
---
## 🔴 2026-09-22 — АВТОЗАКАЧКА ОТКЛЮЧЕНА: `sync-down` убран из крона
**Решение Alex:** закачек быть не должно. Формулировка:
*«ТЕБЕ БЛЯДЬ ЕЩЁ СУКА НЕ ОЧЕВИДНО ЧТО БЛЯДЬ ЗАКАЧЕК БЫТЬ НЕ ДОЛЖНО СКОТИНА?!»*
**Что было:** `watchlist-nightly.sh` вызывал `sync-down --apply --batch 2` →
`sync_down.py` через `radarr_add()` + `radarr_search()` (`sync_down.py:155,158`)
**добавлял фильмы в Radarr/Sonarr и триггерил поиск раздач**. До 2 позиций за прогон.
**Что стало:** вызов `sync-down` вырезан из cron-скрипта.
```
sync-vault → process-thumbs → resolve --recheck --apply → sync-up → sync-vault
```
Бэкап прежнего скрипта: `/home/kraken/scripts/watchlist-nightly.sh.bak-20260922-182655`
Сумма новой версии: `4401882e3b1307a73b3330a70b59ecfe`
🔴 **`sync_down.py` как КОД сохранён** — запускается вручную (`watchlist-sync sync-down
--dry-run` для проверки, `--apply --batch N` для реальной закачки). Отключён
**автоматический прогон по крону**, а не возможность. Возврат в крон — только по
прямому указанию Alex.
**Проверка отсутствия автозакачки:**
```bash
ssh kraken "grep -ln 'radarr_add\|sonarr_add\|radarr_search\|sync-down --apply' \
/home/kraken/scripts/*.sh"
```
Активных вызовов быть не должно — только комментарии.
⚠️ **Radarr уже получил закачки за прошлые прогоны** (9 фильмов). Их судьба не решена.
### Разграничение двух «лимитов» — частый источник путаницы
| Лимит | Что ограничивает | Где |
|---|---|---|
| `batch=N` | сколько позиций **уходит в Radarr на закачку** за прогон | `sync_down.py` (был в кроне) |
| **лимита нет** | **выдача в Lampa** — отдаётся весь `watchlist.json` | `lampac/wwwroot/` |
⚠️ Каталог Lampa строится `.md` → `watchlist.json`; **Radarr в отображении НЕ участвует.**
Когда Alex спрашивал про «лимит обработки/выдачи в lampa» — это НЕ `batch`.
---
## 2026-09-22 — `discover` больше не добавляет кино, которого нет в TMDB (коммит `396acc3`)
**Проблема:** `discover.py` фильтровал новинки **только по рейтингу и голосам**
(IMDb ≥ 7.0, KP ≥ 8.5 для РФ; голосов ≥ 1000 / ≥ 500). Проверки «есть ли фильм
в TMDB» не было → в `## Новинки` попадало вышедшее кино, которого TMDB не знает,
и в каталоге Lampa оно давало «здесь пусто».
**Изменения:**
- `discover.py`: `_resolvable(movie, tmdb_key, kp_key, cache)` — резолв через
`watchlist_sync.resolver` (тот умеет cross-reference RU → `alternativeName` (EN)
→ TMDB). Кэш промахов, чтобы не долбить API.
- `run()`: новый параметр `tmdb_key`. Проверка идёт **после** фильтра по рейтингу,
голосам, году и дедупликации `_all_watchlist_titles()`.
- `upcoming` (`🗓️`) **не проверяется** — календарь информативен, в каталог такие
позиции всё равно не идут.
- `cli.py`: команда `discover` передаёт `TMDB_KEY` из окружения. Без него проверка
не выполняется.
**Живой dry-run** (`cd ~/Developer/watchlist-sync && . ./.env && .venv/bin/python -m watchlist_sync.cli discover`):
из 150 released + 50 upcoming прошло 5 released / 16 upcoming, отсеяно 6 позиций, которых
нет в TMDB: `Одиссея`, `Майкл`, `Убежище`, `Обитель зла`, `Приглашение`, `Властелины вселенной`.
🔴 **Грабли:** правка тела `run()` и правка его сигнатуры — **разные места**. Патч добавил
`tmdb_key` в тело, но не в сигнатуру; Pyright показал `No parameter named "tmdb_key"`
в `cli.py`. Сигнатуру править отдельно и проверять LSP-диагностикой.
Полный контекст задачи (автопоиск, три уровня резолва, `missing.json`, маркер ❓) —
[[personal/plans/watchlist-tmdb-missing-marker]].
---
## Архитектура
```
movies-watchlist.md (Obsidian vault)
│
▼
watchlist-sync (Python CLI)
├── parse — читает .md, классифицирует [ ] / [x] / ❓ / ⬇️
├── resolve — TMDB + KP API → находит ID, тип (movie/series), рейтинг
├── sync-down — непросмотренные → Radarr / Sonarr (батчами)
└── sync-up — Jellyfin watched history → [x] в .md + перемещение наверх
│
▼
Hermes Kraken cron job
├── каждые 6ч: resolve ❓ → LLM disambiguation → уточнение у Alex
└── каждые 24ч: sync-up из Jellyfin
```
---
## Stage 1: Core Sync
### 1.1 Парсинг watchlist.md
**Формат строк:**
- `- [ ] Название` — непросмотрено, не добавлено
- `- [ ] ⬇️ Название` — добавлено в очередь (скачивается/скачано)
- `- [ ] ❓ Название` — ambiguous, ждёт ручного уточнения
- `- [x] Название` — просмотрено
**Источник файла на Кракене:** `/home/kraken/obsidian/family/documents/movies-watchlist.md`
(vault смонтирован в Hermes-контейнере как `/vault`)
### 1.2 Резолв названий (TMDB + KP)
**Алгоритм:**
1. Для каждого `- [ ]` без `⬇️` и `❓` — запрос к TMDB `/search/multi` (русский язык)
2. Если 0 результатов → fallback KP `/v1.4/movie/search?query=...`
3. **Однозначный результат** (score > 0.8, рейтинг ≥ 6.0, один кандидат) → определить тип (movie/tv), взять ID
4. **Ambiguous** (несколько кандидатов с близким score, или score < 0.8) → добавить `❓` перед названием, пропустить
5. **Не найдено** → добавить `❓`, пропустить
**Пороги:**
- ~~`min_rating: 6.0`~~ — ⚠️ **в `resolver.py` этого порога НЕТ.** Фильтр по рейтингу реализован в `sync_down.py` как `MIN_RATING = 6.0` (коммит `ada47da`, 2026-09-20). Резолвер возвращает кандидата с любым рейтингом; отбор происходит на этапе загрузки.
- `min_score: 0.75` — сходство названия (`AMBIG_SCORE_THRESHOLD` в `resolver.py`)
- `AMBIG_DIFF_THRESHOLD: 0.15` — при близких score двух кандидатов → ambiguous
- `max_candidates_for_auto: 1` — при 2+ близких кандидатах → ambiguous (в коде: `candidates[:3]`)
**Тип контента:**
- TMDB `media_type: movie` → Radarr
- TMDB `media_type: tv` → Sonarr
- KP `type: MOVIE/MINI_SERIES/TV_SERIES` → маппинг аналогично
### 1.3 Загрузка (sync-down)
**Батчинг:**
- Максимум **5 новых** в одном прогоне (защита от flood) — задаётся `--batch 5` в `watchlist-nightly.sh`
⚠️ **С 2026-09-22 `sync-down` из крона УБРАН** (автозакачка отключена) — см. раздел выше. `--batch` актуален только для ручного запуска. Исторически стоял `--batch 2` (снижен с 5 после инцидента 2026-09-21: диск 100%).
- ✅ **Приоритет — порядок в файле** (исправлено 2026-09-20, коммит `ada47da`). Раньше стоял `resolved.sort(key=lambda x: x[1].rating or 0, reverse=True)`, из-за чего позиция строки игнорировалась и в батч попадал топ-5 по рейтингу. Сортировка убрана, `resolved` заполняется в порядке `parse()` = сверху вниз.
- ✅ **Фильтр по рейтингу реализован** (там же). Константа `MIN_RATING = 6.0` в начале `sync_down.py`; всё, что ниже, печатается как `[−] … skipped` и не попадает в очередь. **Нулевые рейтинги (`0.0`) отсекаются вместе с остальными** — решение Alex 2026-09-20. На момент замера отсеивалось 18 из 251 кандидатов.
- ⏸ → ✅ **Приоритет `⭐` (U+2B50) РЕАЛИЗОВАН** (2026-09-20, коммит `7db9385`). Двухпроходная выборка: `starred + rest`. Два важных свойства: (а) ⭐ **обходит фильтр `MIN_RATING`** — условие в коде `if "⭐" not in item.raw and rating < MIN_RATING`; (б) `⭐` **срезается из поискового запроса** в `parser._clean()` через `str.replace()` (НЕ через end-anchored класс — маркер стоит в середине названия). Работает из всех секций, `[x]` и под-буллеты не считаются. Детали — в [[family/projects/watchlist-automation]] → «⭐ Приоритет по звёздочке».
- После добавления в Radarr/Sonarr → заменить `- [ ]` на `- [ ] ⬇️` в .md
**API:**
- Radarr: `POST /api/v3/movie` с `addOptions.searchForMovie: true`
- Sonarr: `POST /api/v3/series` с `addOptions.searchForMissingEpisodes: true`
- Проверка дублей перед добавлением (GET by tmdbId/tvdbId)
**Конфиг** (`.env` / `config.json`, не в репо):
```
RADARR_URL=http://localhost:7878
RADARR_KEY=...
SONARR_URL=http://localhost:8989
SONARR_KEY=...
TMDB_KEY=...
KP_KEY=...
JELLYFIN_URL=http://localhost:8096
JELLYFIN_TOKEN=...
WATCHLIST_PATH=/vault/family/documents/movies-watchlist.md
```
### 1.4 Обратная синхронизация (sync-up)
**Алгоритм:**
1. Jellyfin API: `GET /Users/{userId}/Items?IsPlayed=true&IncludeItemTypes=Movie,Series`
2. Для каждого просмотренного — fuzzy-match по названию с `⬇️` строками в .md
3. При совпадении (score > 0.85):
- Заменить `- [ ] ⬇️ Название` → `- [x] Название`
- Переместить строку на **первое место** среди `[x]` записей (под последней `[ ]`)
4. Обновить `.md`, git commit
**Jellyfin userId:** получать через `GET /Users` (admin user = alex)
### 1.5 Cron Jobs (Hermes Eagle)
Два детерминированных shell-script крона:
| Job | Расписание | Скрипт |
|-----|-----------|--------|
| `watchlist-nightly` | ежедн. 01:00 | `~/scripts/watchlist-nightly.sh` |
| `watchlist-discover` | вс 09:00 | `~/scripts/watchlist-discover.sh` |
⚠️ Фактический путь — `~/scripts/`, **не** `~/.hermes/scripts/` (проверено 2026-09-20). Мигрировали с Eagle на Кракен 2026-05-27.
⚠️🔴 **Cron НЕ восстановлен после 2026-06-20** — pipeline запускается только вручную. См. [[family/projects/watchlist-automation]].
---
## Разделение приоритета: порядок / рейтинг / ⭐ (итог обсуждения 2026-09-20)
Три разных механизма, которые легко перепутать:
| Механизм | Роль | Где в коде |
|----------|------|-----------|
| **`⭐` (U+2B50)** | **приоритет поверх всего** — качается первым, **обходит фильтр рейтинга** | `sync_down.py`: `starred + rest`, условие `"⭐" not in item.raw and rating < MIN_RATING` |
| **Порядок в файле** | **приоритет** — что качать раньше (внутри групп) | `sync_down.py` (порядок `parse()`, сортировки нет) |
| **Рейтинг ≥ 6.0** | **отбор** — что вообще качать (кроме ⭐) | `MIN_RATING` в `sync_down.py` |
**Ключевое правило (уточнено Alex 2026-09-20):** порядок влияет на **очерёдность**, рейтинг — на **допуск**. Но `⭐` — это **обход фильтра**, а не просто «в начало очереди»: звёздочка означает явное «хочу», поэтому рейтинг для таких фильмов не проверяется вовсе. Формулировка Alex: «Я блядь сказал в приоритете завела».
**Обработка `⭐` в `parser._clean()`:** маркер может стоять **в середине** названия (`Простите за беспокойство ⭐ 2018`), поэтому срезается через `str.replace('⭐', ' ')`, а НЕ через end-anchored класс `[...]+$` (первая попытка так не сработала). `raw` сохраняет `⭐` — приоритет проверяется именно по `item.raw`.
**`✭` (U+272D) приоритетом НЕ является** — только `⭐` (U+2B50), и только в строке фильма, не в под-буллетах кандидатов; `[x]` пропускается; работает из всех секций.
Используют `~/scripts/sync-vault.sh` для всех git-операций (stash/pull/pop/commit/push). **Не агентные** — скрипты вызывают CLI напрямую, никакого LLM в pipeline.
**Cron: каждые 6 часов**
Промпт агенту на Кракене:
1. Прочитать `.md`, найти все `❓` строки
2. Для каждой — запросить TMDB/KP с расширенными вариантами запроса (LLM генерирует варианты: оригинальное название, транслит, английский перевод)
3. Если нашёл с уверенностью → убрать `❓`, поставить `⬇️`
4. Если нет → собрать список всех нерезолвленных и спросить Alex в Zulip `#personal::Kraken torrents`:
```
❓ Не смог распознать 3 фильма, уточни:
• "Дворец полански" — возможно "The Palace" (Polanski, 2023)? [да/нет/другое]
• "Space merchants" — фильм или книга Пола? Нашёл несколько.
• "Ради нескольких строчек" — не нашёл. Уточни название.
```
---
## Stage 2: Profile & Recommendations
> После накопления истории просмотров (≥ 50 фильмов)
### 2.1 Анализ профиля
- Топ жанры по просмотренным `[x]`
- Любимые режиссёры / актёры (через TMDB credits)
- Средний рейтинг принятых фильмов
- Паттерны: когда смотрит (с женой / один / семья) — вывести из тегов/времени просмотра в Jellyfin
### 2.2 Curated Lists (AI job, еженедельно)
Три списка, **только рейтинг ≥ 6.0**, **не в watchlist уже**:
| Список | Критерии |
|--------|----------|
| 🎬 Вдвоём с Лизой | Драма, комедия, триллер. Без слишком тяжёлого контента. |
| 💀 Один (трэш/боевики) | Хоррор, боевик, sci-fi, cult. Без ограничений. |
| 👨‍👩‍👧 Семейное с ребёнком | Возраст 7+. Мультфильмы, приключения, комедии. |
**Источники кандидатов:**
- TMDB `/movie/recommendations` на основе топ-10 просмотренных
- TMDB `/movie/similar` для любимых жанров
- KP подборки (русское кино / советская классика)
**Формат вывода** — добавить в конец `movies-watchlist.md`:
```markdown
## 🤖 Рекомендации (2026-W21)
### 🎬 Вдвоём
- [ ] Past Lives (2023) ★8.0 — драма о двух корейцах разлучённых в детстве
...
### 💀 Один
- [ ] Nosferatu (2024) ★7.2 — ремейк Эгgers'а
...
### 👨‍👩‍👧 Семья
- [ ] The Wild Robot (2024) ★8.1 — анимация
...
```
После подтверждения Alex — строки перемещаются в основной список.
---
## Структура проекта
> ⚠️ Ниже — **исходный дизайн**. Фактическая структура (проверено 2026-09-20) отличается: `radarr.py`/`sonarr.py` объединены в `arr.py`, добавлен `discover.py`, `recommend.py` не существует (Stage 2 не начата).
**Фактически (репо на Кракене: `/srv/dev-disk-by-uuid-49e8f586-.../watchlist-sync`):**
```
watchlist_sync/
├── __init__.py
├── cli.py — CLI entry (click): resolve / sync-down / sync-up / discover / process-thumbs
├── parser.py — парсинг .md, mark_queued / mark_ambiguous / mark_horror / process_thumbs / _clean
├── resolver.py — TMDB /search/multi + KP fallback, кэш .resolve_cache.json
├── arr.py — Radarr + Sonarr API client (объединены)
├── sync_down.py — непросмотренные → arr (MIN_RATING=6.0, порядок файла, ⭐ приоритет)
├── sync_up.py — Jellyfin → .md
├── discover.py — KP топ новинок → секция «Новинки»
└── jellyfin.py — Jellyfin API client
.env — TMDB_KEY, KP_KEY, RADARR_*, SONARR_* (ZULIP_* ОТСУТСТВУЮТ)
.resolve_cache.json — кэш резолвера (450 записей на 2026-09-20)
.venv/ — py3.11 + httpx + click
pyproject.toml
```
**Дизайн (не реализовано):** `recommend.py` — Stage 2 recommendations.
**Конфиг секреты:** только в `.env` (не в репо).
---
## Имплементация: очерёдность задач
> Статусы проставлены 2026-09-20. Фазы 1-3 и cron реализованы и работают (проверено ручным прогоном `watchlist-nightly.sh`). Stage 2 (Фаза 4) — не начата.
### Фаза 1 (MVP) — ✅ РЕАЛИЗОВАНО
- [x] Инициализировать репо `~/Developer/watchlist-sync`, push на GitHub
- [x] `parser.py` — парсинг .md, классификация строк
- [x] `resolver.py` — TMDB `/search/multi` + KP fallback
- [x] `sync_down.py` — add to Radarr/Sonarr (батч 5), пометить `⬇️`
- [x] CLI: `watchlist-sync resolve --dry-run` и `--apply`
- [x] CLI: `watchlist-sync sync-down --batch 5`
### Фаза 2 — ✅ РЕАЛИЗОВАНО
- [x] `sync_up.py` — Jellyfin → `[x]` + сортировка
- [x] CLI: `watchlist-sync sync-up`
- [x] Cron на Кракене: sync-up каждые 24ч — ⚠️ был, **умер 2026-06-20**, не восстановлен
### Фаза 3 — ✅ РЕАЛИЗОВАНО (кроме Zulip)
- [x] CLI `resolve --recheck` для повторного разбора `❓`
- [ ] AI disambiguation job (Hermes Kraken cron) — не делался, вместо него детерминированный `--recheck`
- [ ] Уточнение у Alex через Zulip для нерезолвленных — код есть (`_zulip_ping()` в `cli.py`), но `ZULIP_*` не в `.env` → молча скипается
### Фаза 4 (Stage 2) — ❌ НЕ НАЧАТА
- [ ] `recommend.py` — профиль + TMDB recommendations
- [ ] Три curated списка с подтверждением
- [ ] Еженедельный cron job
> Файловая структура в разделе «Структура проекта» — тоже дизайн: реально `radarr.py`/`sonarr.py` объединены в `arr.py`, добавлены `discover.py`, `sync_down.py`, `sync_up.py`, `jellyfin.py`, `parser.py`, `resolver.py`, `cli.py`. Точный список — в [[family/projects/watchlist-automation]].
---
## Ключи и секреты
| Секрет | Где хранить |
|--------|-------------|
| TMDB API key | `.env` на Кракене (из `config.json` media-pipeline) |
| KP API key | `.env` (тот же что в media-pipeline config.json) |
| Radarr key | `.env` |
| Sonarr key | `.env` |
| Jellyfin token | `.env` |
Ключи TMDB/KP уже есть в `/srv/.../docker/media-pipeline/config.json` — взять оттуда при деплое.
`.gitignore`:
```
.env
config.json
*.key
__pycache__/
.venv/
```
---
## Связанные проекты
- [[family/projects/media-toolbox-kraken]] — *arr стек, Jellyfin, media-pipeline
- [[family/documents/movies-watchlist]] — источник данных