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

454 lines
30 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
---
## 🔴 2026-09-27: ДЕФЕКТ `[x]`-позиционирования — НАЙДЕН, НЕ ИСПРАВЛЕН
**Симптом (Alex):** «Почему просмотренные типа Даун Хаус или Кокаиновый медведь не падают вниз списка?»
**Корень — `sync_up.py:69`, `_mark_watched()`:**
```python
insert_at = next((i for i, l in enumerate(lines) if re.match(r'\s*-\s*\[x\]', l)), None)
lines.insert(insert_at, raw)
```
`next(...)` = **ПЕРВАЯ** `[x]`-строка **ВСЕГО файла** → вставка идёт **выше** неё.
Комментарий в докстринге говорит *«move to top of `[x]` section»*, а код делает
*«top of file»*. Это и есть баг.
**Спецификация из §1.4 выше:** строка должна вставать **под последней `[ ]`-строкой
главного списка** (первое место среди `[x]`-записей), т.е. в **НАЧАЛО блока
просмотренных**, который тянется вниз до конца `## Фильмы`. Не «в начало файла»,
не «в конец файла», не в отдельную секцию.
**Замер боевого файла (2026-09-27, 712 строк):**
| Что | Значение |
|---|---|
| Секции | `## Новинки - 👍/👎` 20, `## Фильмы` 55, `## Мультфильмы` 676, `## Аниме` 685, `## Новинки👎` 694, `## Связанные заметки` 709 |
| `[x]` в главных секциях | **178** |
| Реальный блок просмотренных | **421–674** |
| `[x]` ВЫШЕ блока (застряли) | **4**: 106 `Кокаиновый медведь`, 109 `Проект конец света`, 111 `Неистовый`, 326 `Даун Хаус` |
Все 4 попали наверх от `sync-from-lampac` (записи после 25.09 — файл датирован 27.09).
Остальные 174 стояли в блоке, и вставка «выше первой `[x]`» для них работала по назначению —
поэтому дефект был незаметен.
**Важно:** правит не только будущие `[x]`, но и накопленные. Разовая нормализация =
все 178 `[x]` главного списка пересобираются блоком от границы последней `[ ]`.
**Готовая функция, которую надо переиспользовать:** `parser.find_main_last_watched(ls)`
(существует, работает только в `process_thumbs`). Требуется инверсия — «последняя `[ ]`».
⚠️ `## Мультфильмы` (676) и `## Аниме` (685) идут **ПОСЛЕ** блока просмотренных →
границу считать **внутри `## Фильмы`**, а не по всему файлу, иначе `[x]` из мультфильмов
поедет не туда.
**Состояние работ на конец сессии:**
- ✅ Коммит ДО правки сделан: **`cf73666`** `sync-down: fall back to name match when KP
card fails; fix question-mark section scoping` — зафиксировано ЧУЖОЕ незакоммиченное
изменение `sync_down.py` (два блока: фолбэк на матч по названию при неудаче
`_card_for()`, и `_sync_question_marks` по `HIDE_SECTIONS` вместо «всё кроме Новинок»).
- ⏸ **Правка `sync_up.py` НЕ сделана.** Требуется апрув.
- Код не тронут: `sync_up.py` md5 `45f2652cfc4c1c550588d52a7d55fd7b` (идентичен в git
HEAD, на Кра́кене и на Mac).
- Установка на Кра́кене — **editable** (`.venv/.../_editable_impl_watchlist_sync.pth`),
правка файла в репе подхватывается без переустановки.
**🔴 Расхождение веток (разобрать до push):** на Mac HEAD = `0534952`
(«feed: fix ❓ marker scope»), на Кра́кене HEAD был `6c5dbe7`. `cf73666` сделан поверх
`6c5dbe7`. Эти два коммита по смыслу перекрываются → `cf73666` **не пушить**, пока не
выяснено, эквивалентен `0534952` или дополняет его.
**Связанные факты сессии:**
- Крон `watchlist-nightly` (01:00) отработал: фид `wwwroot/watchlist.json` — 27.09 01:07,
`count: 403` (films 357 / new 32 / cartoons 7 / anime 7), `missing.json` 3 позиции.
Бэкап `TimeCode.sql` за 27.09 в 01:09 создан.
- `## Новинки` в `.md` не пополнялся с 21.09. Крон discover стоит (`0 9 * * 0`), но
27.09 (воскресенье) `watchlist-discover.sh` залогировал только sync-vault pull/push,
без запуска `discover`. **Причина не выяснена — открытый вопрос.**
- Структура фида: `channels.<code>.items[]` (не массив верхнего уровня) —
`jq -r ".channels | to_entries[] | \"\(.key): \(.value.items | length)\""`. Поле
`count: 403` в корне. 🔴 `jq -c ".channels.films | .[0]"` даёт
`Cannot index object with number` — канал это объект `{title, items}`, не массив.
- В фиде нет ни одного `[x]` (проверено: `Даун Хаус`, `Кокаиновый медведь`, `Неистовый`
отсутствуют) — фид строится только из `Status.UNWATCHED` (`sync_down.py:326`).
**Порядок работ (регламент Alex, повторён явно):** коммит ДО → правка → деплой → прогон.
Бэкап в план НЕ входил (агент добавил отсебятиной, Alex возразил) — не повторять.
Спроектированная, но НЕ выполненная папка `~/backup/reflow-watched-20260927-215836/`
на Кра́кене (3 файла: `sync_up.py`, `cli.py`, `movies-watchlist.md`) — можно снести.
**Питфолл сессии (для будущих агентов):** Alex спросил «куда именно вниз», имея в виду
**документацию**. Ответ лежал в §1.4 этой же заметки. Правило: при вопросе «по
документации?» — читать доки, а не предлагать варианты на выбор.
**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]] — источник данных