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

23 KiB
Executable File
Raw Blame History

created, updated, status, tags
created updated status tags
2026-05-18 2026-09-20 active
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.

Проверка отсутствия автозакачки:

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:

## 🤖 Рекомендации (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) — ✅ РЕАЛИЗОВАНО

  • Инициализировать репо ~/Developer/watchlist-sync, push на GitHub
  • parser.py — парсинг .md, классификация строк
  • resolver.py — TMDB /search/multi + KP fallback
  • sync_down.py — add to Radarr/Sonarr (батч 5), пометить ⬇️
  • CLI: watchlist-sync resolve --dry-run и --apply
  • CLI: watchlist-sync sync-down --batch 5

Фаза 2 — ✅ РЕАЛИЗОВАНО

  • sync_up.py — Jellyfin → [x] + сортировка
  • CLI: watchlist-sync sync-up
  • Cron на Кракене: sync-up каждые 24ч — ⚠️ был, умер 2026-06-20, не восстановлен

Фаза 3 — ✅ РЕАЛИЗОВАНО (кроме Zulip)

  • 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/

Связанные проекты