--- 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 (рекомендации). Ниже сохранён дизайн; расхождения с реальностью помечены. --- ## Архитектура ``` 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-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]] — источник данных