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

52 KiB
Raw Blame History

created, updated, status, tags
created updated status tags
2026-05-20 2026-09-21 active
kraken
watchlist
automation
movies

Watchlist Automation — Полный Flow

Полная цепочка: как family/documents/movies-watchlist.md превращается в скачанный контент.

Полная цепочка

[вс 09:00] watchlist-discover  ←  KP API топ фильмов 2024-2025
                  ↓ добавляет в секцию "Новинки 👍/👎"
         movies-watchlist.md
         Alex ставит 👍 / 👎
                  ↓
[ежедн 09:00] watchlist-resolve  ← SSH Кракен → resolve → commit
[ежедн 10:00] watchlist-sync-down
  → process-thumbs (перемещает 👍 → начало списка)
  → sync-down (добавляет батч в Radarr/Sonarr)
                  ↓
Radarr (7878) / Sonarr (8989)
  → Prowlarr (9696) → RuTracker / Kinozal(M) / NoNaMe / etc.
  → Transmission (9091)
  → /media/movies-radarr или /media/series-sonarr
                  ↓
router.py (Custom Script) → читает жанры из NFO/TMDb
  → symlink в /media/{movies,cartoons,documentaries,...}/
                  ↓
Jellyfin (8096) → rescan → виден контент

Статусы в movies-watchlist.md

Символ Значение Действие
[ ] не смотрел, в очереди sync-down добавляет в Radarr/Sonarr
[x] просмотрено sync-up из Jellyfin watched history
⬇️ добавлено в Radarr/Sonarr ждёт скачки
👍 нравится (новинка) process-thumbs переносит в начало → попадёт в sync-down
👎 не хочу (новинка) process-thumbs перемещает в ## Новинки👎
🗓️ upcoming (ещё не вышел) discover помечает, не скачивается
❓ не найдено / ambiguous ждёт ручного уточнения
🧟‍♀️ horror (genre_id=27) информационный маркер
⭐ приоритет к загрузке (U+2B50) sync-down берёт в первую очередь, фильтр рейтинга не применяется ✅ реализовано 2026-09-20

⚠️ ✭ (U+272D) — НЕ приоритет. Alex 2026-09-20 явно указал: звёздочкой-приоритетом считается только ⭐ (U+2B50). Символ ✭ используется в файле как обычная пометка «выделенное», приоритетом не является.

Также: ⭐ засчитывается только в строке фильма (- [ ] Название ⭐ …), НЕ в под-буллетах кандидатов, которые генерирует mark_ambiguous (там формат «Название (год) ★N.N»). [x] (просмотренные) с ⭐ игнорируются. Работает из всех секций (## Фильмы, ## Новинки, ## Мультфильмы, ## Аниме).

Правила отбора в sync-down (актуально с 2026-09-20)

Приоритет определяется двумя факторами, а не позицией строки:

Условие Поведение
⭐ (U+2B50) в строке фильма первыми, фильтр рейтинга не применяется
все остальные [ ] по порядку файла (сверху вниз), рейтинг ≥ 6.0
рейтинг < 6.0 (включая 0.0) отсеивается, с логом [−] … skipped
[x] пропускается
❓ / 🗓️ (ambiguous) пропускается, обрабатывается отдельно

⚠️ Порядок файла ≠ рейтинг. До 2026-09-20 код сортировал по рейтингу TMDB и игнорировал позицию в списке — из-за этого в батч попадали фильмы «не из начала списка». Исправлено коммитом ada47da.

⚠️ ⭐ = обход фильтров, а не смена сортировки. Это принципиальная позиция Alex («Я блядь сказал в приоритете завела»): звёздочка означает явное «хочу», поэтому рейтинг для неё не проверяется вовсе.

Структура секций movies-watchlist.md

## Новинки - 👍/👎   ← discover добавляет сюда
## Фильмы            ← основной список, отступ 2 пробела (  - [ ])
## Мультфильмы
## Аниме
## Новинки👎         ← без пробела перед 👎
## Связанные заметки

Важно: process_thumbs автоматически определяет отступ из ## Фильмы и сохраняет при переносе. SECTION_THUMBDN = "## Новинки👎" (без пробела) — должно точно совпадать с заголовком.

✅ Крон pipeline ВОССТАНОВЛЕН (2026-09-20 21:21)

Задача закрыта. Pipeline был мёртв с 2026-06-20 (проверено аудитом ниже), теперь расписание вернули.

Установлено в crontab юзера kraken:

# Watchlist: process-thumbs + resolve + sync-down + sync-up (ежедневно 01:00)
0 1 * * * bash /home/kraken/scripts/watchlist-nightly.sh >> /tmp/watchlist-nightly.log 2>&1

# Watchlist: discover — новинки из KP API (воскресенье 09:00)
0 9 * * 0 bash /home/kraken/scripts/watchlist-discover.sh >> /tmp/watchlist-discover.log 2>&1

Как ставилось (идемпотентный скрипт, бэкап перед установкой):

crontab -l > ~/crontab-backup-$(date +%Y%m%d-%H%M%S).txt   # бэкап
crontab /tmp/crontab-new.txt                                 # установка
crontab -l                                                   # проверка

Бэкап: ~/crontab-backup-20260920-212155.txt

Проверено после установки:

  • systemctl is-active cron → active
  • grep -c watchlist /var/spool/cron/crontabs/kraken → 2 (обе строки подхвачены демоном)
  • Логи пишутся в /tmp/ — теряются при перезагрузке. Перенести в постоянное место, если нужно сохранять историю.

⚠️ В docker exec hermes-kraken hermes cron list по-прежнему «No scheduled jobs» — это НЕ про watchlist. Watchlist-скрипты детерминированные, живут в системном cron, в Hermes-планировщике их не должно быть. Не искать их там.

🔴 Аудит 2026-09-20: как выглядело состояние ДО восстановления (историческая справка)

На момент аудита автодобавление фильмов НЕ работало по расписанию. Скрипты были живы, но их никто не запускал.

Доказательства (проверено 2026-09-20):

  • crontab -l юзера kraken — watchlist-строк НЕТ. Есть только sync-vault.sh (каждый час) и два pgs-to-srt (часовой + 03:00).
  • root crontab и /etc/cron.d/: grep -rl watchlist → пусто.
  • systemd timers (user + system) → 0 штук.
  • Внутри Hermes-контейнера на Кракене: docker exec hermes-kraken /opt/hermes/.venv/bin/hermes cron list → "No scheduled jobs."
  • Последний коммит автором Hermes (= nightly-скрипт) — 2026-06-20. Распределение по месяцам: май 1, июнь 9, июль/август/сентябрь — 0. Единственные коммиты в vault после июня — от eagle, syncthing, taiga.

Скрипты существуют и исполняемы, но не в кроне:

~/scripts/watchlist-nightly.sh    mtime 2026-05-29
~/scripts/watchlist-discover.sh   mtime 2026-05-29

⚠️ Изначальная гипотеза «файл засран от многомесячного простоя» — НЕВЕРНА. Дубли создавались на каждом прогоне resolve из-за бага в mark_ambiguous() (см. ниже, исправлен). 33 строки у «Nightmare Alley» = 11 прогонов. Простой лишь позволил мусору накопиться незамеченным. Мусор вычищен, баг закрыт.

✅ Баг дублирования кандидатов в mark_ambiguous() — НАЙДЕН И ИСПРАВЛЕН (2026-09-20)

Симптом

Под каждой ❓-строкой копились одинаковые дампы кандидатов. Рост ровно +100 байт за прогон (3 строки кандидатов) — подтверждено экспериментом: 26430 → 26530 → 26630 → 26730 байт.

Корень (точная механика)

В watchlist_sync/parser.py, функция mark_ambiguous():

pattern = re.compile(
    r'(^\s*-\s*\[ \]\s*)[❓]?\s*(' + escaped + r')\s*$',   # ← матчит ТОЛЬКО строку ❓
    re.MULTILINE
)
...
replacement = rf'\1❓ \2\n{cand_lines}'                    # ← новые кандидаты
new_text = pattern.sub(replacement, text, count=1)        # ← ВСТАВЛЯЮТСЯ перед старыми

Паттерн снимал только саму ❓-строку. Замена вставляла \n + 3 новых кандидата, а старые 33 строки оставались ниже — реплейс не знал о них. Итог: каждый прогон добавляет ещё один блок кандидатов поверх существующих.

Важно: ❓-строки НЕ дублировались (каждая — одна). Дублировались под-буллеты под ними.

Фикс

Паттерн теперь захватывает весь блок: строку-маркер ПЛЮС её под-буллеты, и перезаписывает целиком.

# Candidate sub-bullet: indent of 4+ spaces, a bullet, NOT a checkbox line.
# Each iteration REQUIRES its own leading newline -- otherwise `*` walks onto
# the following line and swallows neighbouring items.
_SUB_BULLETS = r'(?:\n[ \t]{4,}-[ \t]*(?!\[[ xX]\])[^\n]*)'

pattern = re.compile(
    r'(^[ \t]*-[ \t]*\[ \][ \t]*)[❓]?[ \t]*(' + escaped + r')[ \t]*'
    r'(?P<subs>' + _SUB_BULLETS + r'*)',
    re.MULTILINE
)

⚠️ Две НЕУДАЧНЫЕ версии паттерна (не повторять)

  1. v1 — (?:\n[ \t]+-[^\n]*\n?)*: \n? опционален, поэтому * перешагивал на следующую строку без разделителя и проглатывал соседние фильмы. Потеряны ❓ The kill room, The burial, Past lives. Откачено.
  2. v2 — то же плюс негативный lookahead: не помогло, [^\n]* из предыдущей итерации уже захватил хвост следующей строки.
  3. v3 (рабочая) — перенос обязателен внутри каждой итерации ((?: + \n + ...) + *), якорь не нужен.

Правило для парсеров этого файла: под-буллет кандидата = отступ ≥4 пробела, это буллет и он НЕ чекбокс (- [ ] / - [x]). Строки фильмов имеют отступ 2 пробела — именно поэтому их надо исключать явно.

Проверка фикса

На чистой копии (md5 40295fac...) — 3 прогона mark_ambiguous():

run 1: size=25430
run 2: size=25430
run 3: size=25430

Размер стабилен. Потеряно 0 не-кандидатных строк. Past lives на месте. 838 → 808 строк (минус 30 = старые дубли, 33 → 3).

Чистка существующих дублей

Скрипт /tmp/wl_dedupe.py (дедуп по title|year|rating, первое вхождение сохраняется, порядок не меняется; строки фильмов не трогаются). Прогон --apply:

  • 168 дублей в 15 блоках, 26430 → 21032 байт (838 → 670 строк).
  • Топ: Nightmare alley 33→3, Stand by me 30→3, Фарго 30→3, Left behind 21→4, Крестный отец 12→3, Godfathers 12→3.

Ручной прогон watchlist-nightly.sh (от юзера kraken)

bash ~/scripts/watchlist-nightly.sh

Прошёл до Done, без падений. Результат: 270 resolved (38 horror), 37 ambiguous, 22 not found. sync-down добавил 4 фильма в Radarr (Американская история X, Мастерская сатаны, Я ругаюсь, Гладиатор), Scream уже был → помечен ⬇️. sync-up: 7 played items из Jellyfin, 1 отмечен. Vault закоммичен и запушен (Committed: family/documents/movies-watchlist.md, Pushed ✓).

После прогона дублей: 0 (проверено отдельным скриптом). Секции целы: Новинки / Фильмы / Мультфильмы / Аниме / Новинки👎 / Связанные заметки.

🔴 БАГ: sync-down игнорирует порядок файла — сортирует по рейтингу (найден 2026-09-20)

Симптом (сообщил Alex): в батч на закачку попали фильмы, которые «далеко не в начале списка» — Гладиатор, Американская история X, Я ругаюсь, Мастерская Сатаны.

Корень: watchlist_sync/sync_down.py:41:

resolved.sort(key=lambda x: x[1].rating or 0, reverse=True)

Код полностью игнорирует позицию строки в файле и берёт топ-5 по рейтингу TMDB/KP из всех неотмеченных [ ]-строк. Порядок списка не имеет значения вообще.

Расхождение с докой: в watchlist-sync §1.3 заявлено «Приоритет: порядок в файле (выше = важнее)» — это не соответствует коду. Реальный приоритет = рейтинг.

Вторая половина причины: в том же прогоне process-thumbs выполняется до sync-down и переносит 👍-позиции из ## Новинки в начало основного списка. Среди перенесённых оказались Мастерская Сатаны (8.3) и Я ругаюсь (8.2) — с высокими рейтингами, поэтому они сразу выиграли отбор.

Как считался батч 2026-09-20 (5 слотов):

Слот Фильм Рейтинг Итог
1 Крик / Scream 10.0 уже в Radarr → помечен ⬇️
2 Волшебный участок 9.0 400 «already added» в Sonarr
3 Гладиатор 8.225 добавлен
4 Я ругаюсь 8.229 добавлен
5 Мастерская Сатаны 8.3 добавлен
— Американская история X ~8.2 добавлен (слот освободился после первых двух)

Saving Private Ryan (8.223) и Иди и смотри (8.214) проиграли Гладиатору (8.225) на тысячные доли.

❌ В sync_down ОТСУТСТВУЕТ фильтр по рейтингу

Проверено grep -rn "6\.0\|min_rating" watchlist_sync/*.py:

  • cli.py:92 — < 6.0 только для сброса кэша при --recheck, не отбор
  • discover.py:105,203 — фильтр только для шага discover
  • resolver.py:264 — 0.5 это сравнение рейтингов при ничьей, не порог

В sync_down порога нет вообще. Порог min_rating: 6.0 из доки watchlist-sync §1.2 в коде не реализован. Итог: в очередь могут попасть фильмы с рейтингом 0.0-5.9.

Замер 2026-09-20: из 251 resolved-кандидатов 18 имеют рейтинг < 6.0 — например Шестой элемент 4.3, Мегалополис 5.2, Бешеная любовь 3.1, Ворон 2024 5.8.

⚠️ Отдельный случай: rating == 0.0 означает «TMDB не дал оценку», а НЕ «плохой фильм». Таких двое: Уроки благи, Годзилла и конг. Наивный фильтр < 6.0 их отсечёт и потеряет нормальное кино.

✅ РЕАЛИЗОВАНО (2026-09-20): sync_down следует порядку файла + фильтр MIN_RATING

План из «📋 ПЛАН» выполнен. Alex подтвердил: порядок файла = приоритет, рейтинг = отбор, нулевые рейтинги (0.0) отсекать вместе с остальными ниже порога.

Коммиты в репо watchlist-sync (оба сделаны в этой сессии):

ada47da  feat(sync-down): follow file order, filter by MIN_RATING
2dab180  fix(parser): mark_ambiguous replaces candidate block instead of prepending

Предыдущий HEAD до сессии — 20ca511. Рабочее дерево чистое.

Правка 1 — убран resolved.sort() (был на строке 41). resolved заполняется циклом по parse(), то есть остаётся в порядке файла сверху вниз.

Правка 2 — добавлена константа и фильтр в начало sync_down.py:

# Minimum TMDB/KP rating for a title to be queued. 0.0 means "no rating in
# the database" -- per Alex it is discarded together with everything below
# the threshold, so unrated items never reach Radarr/Sonarr.
MIN_RATING = 6.0
if result.status == "ok":
    if (result.candidate.rating or 0) < MIN_RATING:
        skipped_low += 1
        print(f"  [−]  {item.title!r} — rating "
              f"{result.candidate.rating or 0:.1f} < {MIN_RATING}, skipped")
        continue
    resolved.append((item, result.candidate))
else:
    deferred.append((item, result))

Плюс итоговая строка (N item(s) below rating 6.0 skipped) — чтобы отсев был виден в логе, а не молчал.

process_thumbs сознательно НЕ тронут — 👍 по-прежнему уезжают в верх основного списка, то есть свежие новинки намеренно идут первыми (выбор Alex, «вариант 1»).

Dry-run после правки (проверено):

[−] ... 18 строк «skipped» (все < 6.0, включая два нуля: Уроки благи 0.0, Годзилла и конг 0.0)
(18 item(s) below rating 6.0 skipped)
[→] 'Под огнём' (2025) ★7.1        [dry-run]
[→] 'Секретный агент' (2025) ★7.3  [dry-run]
[→] 'Охота на воров 2: Пантера' (2025) ★6.6  [dry-run]
[→] 'Гадкая сестра' (2025) ★7.3    [dry-run]
[→] 'Семья в аренду' (2025) ★7.8   [dry-run]
Queued 5/5 items [dry-run]

Порядок ровно как в файле, с самого верха ## Новинки. Рейтинг больше не влияет на очерёдность — только на отбор.

⚠️ Реальный прогон (bash ~/scripts/watchlist-nightly.sh) после этой правки был ПРЕРВАН — не отработал. Vault проверен: рабочее дерево чистое, movies-watchlist.md остался на коммите d7ce0a9 (результат первого прогона), частичных изменений нет.

✅ ⭐ Приоритет по звёздочке ⭐ (U+2B50) — РЕАЛИЗОВАНО (2026-09-20)

Требование Alex: фильмы, отмеченные звёздочкой ⭐, идут в первую очередь к загрузке — приоритет поверх порядка файла, из всех секций списка.

Правила (финальные, подтверждены Alex):

  • Только символ ⭐ (U+2B50). Символ ✭ (U+272D) приоритетом НЕ является.
  • Только строки фильмов (- [ ] Название ⭐ …), НЕ под-буллеты кандидатов от mark_ambiguous («Название (год) ★N.N»).
  • [x] (просмотренные) — пропускаются.
  • Из всех секций — не только ## Фильмы/## Новинки (уточнение Alex).
  • ⭐ обходит фильтр MIN_RATING — «Я блядь сказал в приоритете завела». Звёздочка = явное «хочу», рейтинг не важен.
  • ⭐ срезается из поискового запроса перед отправкой в TMDB (очевидное требование Alex).

Коммит: 7db9385 feat: ⭐ (U+2B50) items are queued first, bypassing the rating filter

Правка 1 — parser.py::_clean(). ⚠️ Найденный баг: ⭐ стоит в середине названия (Простите за беспокойство ⭐ 2018), а старый regex был привязан к концу строки (\s*[...]+\s*$) — поэтому класс не матчился и символ утекал в запрос TMDB. Работало случайно (TMDB прощал лишний глиф).

# ⭐ (U+2B50) is a priority marker and may sit ANYWHERE in the title
# ("Простите за беспокойство ⭐ 2018"), so strip it without the end anchor.
s = s.replace('⭐', ' ')
s = re.sub(r'\s+', ' ', s).strip()
s = re.sub(r'\s*[✭★☆✩✪✫✬✮✯✰]+\s*$', '', s).strip()   # остальные звёзды — по-прежнему end-anchored
return s

Критично: ⭐ вырезается из title (чистый запрос), но остаётся в raw — приоритет проверяется по item.raw, поэтому оба свойства сохраняются.

Правка 2 — sync_down.py, двухпроходная выборка:

# ⭐ (U+2B50) marks a must-have: such items are queued FIRST and bypass
# the rating filter entirely. Order within each group stays file order.
starred = [i for i in candidates_raw if "\u2b50" in i.raw]
rest    = [i for i in candidates_raw if "\u2b50" not in i.raw]
if starred:
    print(f"  ⭐ {len(starred)} priority item(s) {[i.title for i in starred]}")

for item in starred + rest:
    result = resolve(item.title, tmdb_key, kp_key)
    if result.status == "ok":
        if "\u2b50" not in item.raw and (result.candidate.rating or 0) < MIN_RATING:
            skipped_low += 1
            print(f"  [−]  {item.title!r} — rating "
                  f"{result.candidate.rating or 0:.1f} < {MIN_RATING}, skipped")
            continue
        resolved.append((item, result.candidate))
    else:
        deferred.append((item, result))

Обрати внимание на условие: "\u2b50" not in item.raw and rating < MIN_RATING — фильтр применяется только к не-⭐. Это и есть «обходит фильтр».

Итоговые правила отбора в sync-down:

⭐ обычные
Очерёдность первыми после, по порядку файла
Фильтр MIN_RATING = 6.0 не применяется применяется
[x] пропуск пропуск

Проверка (2026-09-20):

_clean('Простите за беспокойство ⭐ 2018') -> 'Простите за беспокойство 2018'   (⭐ срезан)
raw:   'Простите за беспокойство ⭐ 2018'    star in raw: True    (приоритет работает)
title: 'Простите за беспокойство 2018'       star in title: False (чистый запрос)
candidates: 250 | starred: 1 | rest: 249 — ⭐ первым в итерации

Dry-run после правки:

⭐ 1 priority item(s) ['Простите за беспокойство 2018']
  [→] 'Простите за беспокойство 2018' → 'Простите за беспокойство' (movie, 2018, ★6.8) [dry-run]
  [→] 'Гадкая сестра' (2025) ★7.3 ...
Queued 5/5 items [dry-run]

Единственный ⭐ в файле — строка 79, секция ## Фильмы: - [ ] Простите за беспокойство ⭐ 2018.

✅ Реальный прогон 2026-09-20 19:53 — прошёл чисто

bash ~/scripts/watchlist-nightly.sh от юзера kraken → Done, ошибок нет. Вывод (/tmp/wl-run.log):

(18 item(s) below rating 6.0 skipped)
  [=]  'Простите за беспокойство' already in Radarr, marking queued    ← ⭐ обработан ПЕРВЫМ
  [=]  'Гадкая сестра' already in Radarr, marking queued
  [✅] 'Список заветных желаний' added to Radarr, search triggered
  [✅] 'Отпуск на двоих' added to Radarr, search triggered
  [✅] 'Простая случайность' added to Radarr, search triggered
Queued 5/5 items
[19:53:32] ▶ Done

⭐-фильм попал в очередь первым, но добавлять было нечего (уже в Radarr с прерванного прогона) — значит приоритет сработал. Дубли после прогона: 0. Файл: 21332 байт / 676 строк.

🩹 Почему первая версия правки не сработала (не повторять)

Первая попытка добавить ⭐ в существующий класс [✭★☆✩✪✫✬✮✯✰] не дала эффекта — класс end-anchored ($), а ⭐ стоит в середине названия. Проверка на тестах показала star gone: False. Вывод: для маркеров, которые могут стоять в любой позиции, нужен str.replace(), а не класс с якорем.

📋 Как задача ставилась (для контекста будущих сессий)

Alex формулировал требования короткими репликами, и план сначала был неверно истолкован дважды:

  1. «⭐ - в приоритете, из всех списков. Естественно в tmdb ее срезать» — уточнение, что секции не ограничены и глиф не должен уходить в запрос.
  2. «Что блядь значит рейтинг остался?! Я блядь сказал в приоритете завела» — категорическое: ⭐ обходит фильтр рейтинга, а не просто меняет очерёдность.

Вывод для будущего: когда Alex говорит «в приоритете» — это означает обход фильтров, а не смену сортировки. Уточнять это ДО предложения плана, а не после.

📦 Результат закачки по итогам прогона 2026-09-20

sync-down добавил 4 фильма (Scream уже был → ⬇️). Проверено через Radarr API + Transmission:

Фильм Взятый релиз Статус
Gladiator (2000) UHD BDRip [H.265/2160p] [4K] качается
American History X (1998) BDRip [AVC] качается
I Swear (2025) / Я ругаюсь BDRip [H.264/1080p] [DVO] качается
Oficina do Diabo (2025) / Мастерская Сатаны — релиз не найден, ждёт RSS

✅ Профиль качества работает как задумано: Gladiator взят в 4K, но BDRip, не Remux — профиль 7 WEB-4K (no remux) фильтрует корректно.

Застрявшие в очереди Radarr (со статусом warning, к этому прогону не относятся): Brokeback Mountain, Мортал Комбат 2, Sorry to Bother You, Город бога, День хомячка. Отдельная задача — разобрать.

🟡 Побочная находка: Zulip-пинг не настроен на Кракене

_zulip_ping() требует ZULIP_URL, ZULIP_API_KEY, ZULIP_BOT_EMAIL, но их нет в .env репо watchlist-sync. При 59 нерезолвленных items → [warn] ... not set — skipping ping. Уведомления о нерезолвленных до Alex не доходят.

🟡 Побочная находка: Sonarr 400 «This series has already been added»

При sync-down для «Волшебный участок»: 400 Bad Request на /sonarr/api/v3/series. Причина безобидная — серия уже в Sonarr (tvdbId=442038, «Magical Police Department»). arr.py::sonarr_add() не проверяет дубли перед POST, поэтому Sonarr отвечает SeriesExistsValidator. Не сбой, но шумит в логе — стоит добавить pre-check (как в radarr_add).

Бэкапы (перед правками)

~/obsidian/family/documents/movies-watchlist.md.bak-20260920-192909          (838 строк, 26430 б)
~/obsidian/family/documents/movies-watchlist.md.dedupe-backup-20260920-193111
/srv/.../watchlist-sync/watchlist_sync/parser.py.bak-20260920-192909

Статус на конец сессии (2026-09-20, финал)

Три коммита в репо watchlist-sync (порядок применения):

7db9385  feat: ⭐ (U+2B50) items are queued first, bypassing the rating filter
ada47da  feat(sync-down): follow file order, filter by MIN_RATING
2dab180  fix(parser): mark_ambiguous replaces candidate block instead of prepending
20ca511  ← HEAD до сессии

Рабочее дерево чистое. .bak-* добавлены в .gitignore.

  • ✅ Фикс parser.py (mark_ambiguous) применён, проверен 3 прогонами на чистой копии, закоммичен.
  • ✅ Фикс sync_down.py — порядок файла + MIN_RATING = 6.0, закоммичен. Dry-run подтверждён.
  • ✅ Приоритет ⭐ (U+2B50) реализован, закоммичен. Обходит фильтр рейтинга, работает из всех секций, [x] пропускает.
  • ✅ Файл watchlist вычищен (168 дублей → 0) и закоммичен в vault.
  • ✅ Два реальных прогона watchlist-nightly.sh прошли чисто. Второй (19:53) — уже с приоритетом ⭐: Done, ошибок нет, 3 фильма ушли в закачку.
  • ✅ Дубли после всех прогонов: 0.
  • 🔴 Крон по-прежнему отсутствует — pipeline живёт только вручную. ГЛАВНАЯ незакрытая задача.
  • 🟡 Zulip-пинг не настроен (ZULIP_* нет в .env).
  • ℹ️ Доки watchlist-automation и watchlist-sync обновлялись агентом Eagle в этой же сессии (коммиты bd743da, abee5a8 в vault) — при правках сверяться.

Хронология прогонов 2026-09-20:

Время Что Результат
~19:33 Прогон после фикса дедупа (до правки sync_down) 4 фильма в Radarr: Gladiator, American History X, I Swear, Oficina do Diabo (без релиза)
~19:45 Прогон после правки sync_down ПРЕРВАН (прерван пользователем), правок не оставил
19:53 Финальный прогон (файловый порядок + MIN_RATING + ⭐) Done. +3 фильма: Список заветных желаний, Отпуск на двоих, Простая случайность

Бэкапы (все созданы в этой сессии):

~/obsidian/family/documents/movies-watchlist.md.bak-20260920-192909            (838 строк, 26430 б — ДО фикса)
~/obsidian/family/documents/movies-watchlist.md.dedupe-backup-20260920-193111  (до дедупа)
/srv/.../watchlist-sync/watchlist_sync/parser.py.bak-20260920-192909           (md5 0f597606fa14f357ff3b25547bd18ea6)
/srv/.../watchlist-sync/watchlist_sync/parser.py.bak-20260920-195100           (до правки ⭐)
/srv/.../watchlist-sync/watchlist_sync/sync_down.py.bak-20260920-194447
/srv/.../watchlist-sync/watchlist_sync/sync_down.py.bak-20260920-195100        (до правки ⭐)

Скрипты диагностики/фикса (на Кракене, /tmp/): wl_dedupe.py (дедуп кандидатов, --apply), wl_postcheck.py (проверка дублей после прогона), wl_orphan.py (проверка структуры блоков), wl_verify_final.py, wl_order2.py, wl_grab_check.py (Radarr queue + Transmission), wl_fix_parser3.py, wl_fix_syncdown.py, wl_fix_star.py, wl_fix_clean2.py, wl_star.py, wl_star_check.py, wl_final_star.py.

Скрипты повторного использования: wl_dedupe.py — при накоплении дублей; wl_postcheck.py — контроль после каждого прогона; wl_grab_check.py — что реально ушло в закачку.

Пути к конфигам *arr (проверено 2026-09-20, в доке ранее были ошибочны)

Конфиги Radarr/Sonarr/Transmission лежат на втором диске (6194539b-03e0-4f33-986b-4da09834e82f), не на том же, что медиа:

Radarr API key:  /srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config/config.xml
Sonarr:          /srv/dev-disk-by-uuid-6194539b-.../docker/sonarr/config/config.xml
Transmission:    /srv/dev-disk-by-uuid-6194539b-.../docker/transmission/settings.json
Radarr API base: http://localhost:7878/radarr/api/v3/  (ключ из config.xml)
Sonarr API base: http://localhost:8989/sonarr/api/v3/

Проверка закачки: queue в Radarr показывает grabbed/downloading; history даёт точное имя взятого релиза. Transmission — docker exec transmission transmission-remote --list (RPC-юзер в settings.json пустой).

Cron Jobs (на Кракене, восстановлены 2026-09-20, дополнены 2026-09-21)

Весь watchlist pipeline работает на Кракене (мигрировал с Eagle 2026-05-27). Крон был мёртв с 2026-06-20 по 2026-09-20, восстановлен (детали в начале документа).

Job Расписание Скрипт
watchlist-nightly ежедн. 01:00 ~/scripts/watchlist-nightly.sh
watchlist-cleanup ежедн. 02:00 ~/scripts/watchlist-cleanup.sh --apply
watchlist-discover вс 09:00 ~/scripts/watchlist-discover.sh

⚠️ Фактический путь — ~/scripts/, не ~/.hermes/scripts/. Оба скрипта вызывают CLI напрямую через uv run --project $WS, без LLM. uv лежит в ~/.local/bin/uv.

⚠️ ~/scripts/ — root:root 755. Добавить/заменить скрипт можно только sudo install -m 755 (у kraken sudo без пароля). Обычная запись (>, cp) откажет с Permission denied.

⚠️ watchlist-cleanup.sh без аргумента = dry-run. В кроне обязателен --apply, иначе удаление не произойдёт и лог будет писать «nothing to delete». Это самая тихая из возможных поломок — проверять флаг при любой правке крона.

WS в обоих скриптах: /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync — другой UUID, чем фактический корень репо (см. ниже). Возможный источник поломки: проверить, что путь резолвится.

watchlist-nightly.sh (на Кракене):

  1. sync-vault.sh (pull) → process-thumbs --apply → sync-vault.sh (push)
  2. resolve --recheck --apply → sync-down --apply --batch 5 → sync-up --apply → sync-vault.sh (push)

Пути на Кракене (HDD UUID 49e8f586-3839-4c5d-a1e1-58bfc3579ade):

  • /home/kraken/obsidian → symlink на /srv/dev-disk-by-uuid-.../obsidian
  • /home/kraken/Developer/watchlist-sync → symlink на /srv/dev-disk-by-uuid-49e8f586-.../watchlist-sync

✅ Путь репо в скриптах ВЕРНЫЙ (проверено 2026-09-20). В watchlist-nightly.sh и watchlist-discover.sh:

WS=/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync
UV=~/.local/bin/uv

ls -la подтверждает: /srv/.../watchlist-sync существует, внутри watchlist_sync/, .venv/, .resolve_cache.json, pyproject.toml, .env (есть, содержит TMDB_KEY/KP_KEY/RADARR_/SONARR_; нет ZULIP_*).

Ранее в доке фигурировал путь .../6194539b-.../watchlist-sync «как реальный корень». На 2026-09-20 рабочий путь — 49e8f586, и он же используется в скриптах. Не «исправлять» его обратно.

Ещё одно подтверждённое отличие: ~/scripts/ — реальная директория (drwxr-xr-x root root), а ~/.hermes/ на Кракане содержит только skills/. Скрипты лежат в ~/scripts/, не в ~/.hermes/scripts/.

Как запускать CLI вручную (рабочая команда)

ssh kraken
cd /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync
uv run --project . watchlist-sync <команда>        # resolve / sync-down / sync-up / process-thumbs / discover

Или целиком пайплайн: bash ~/scripts/watchlist-nightly.sh

Discover — Фильтрация

Тип Порог
Мировые IMDb ≥ 7.0 или KP ≥ 7.0
Российские KP ≥ 8.5 (защита от накрутки)
Upcoming год ≥ текущий−1
Минимум голосов IMDb ≥ 1000 или KP ≥ 500

KP API endpoint: https://api.poiskkino.dev/v1.4/movie (redirect с api.kinopoisk.dev).

*arr Stack — Индексеры (Prowlarr)

ID Имя Тип Примечание
1 RuTracker semi-private блокирует при rate limit на сутки
2 RuTor public поиск по imdbid
3 Byrutor public
4 NoNaMe Club semi-private анонимный
5 Kinozal semi-private резерв
6 Kinozal (M) semi-private магнет ✅ — без grab limit, предпочтительный

Pi-hole питфолл: блокирует skyhook.sonarr.tv → Sonarr/Radarr поиск молча не запускается. Фикс: dns: [8.8.8.8] в docker-compose.yml.

Router Script — Маршрутизация по жанру

После импорта Sonarr/Radarr: router.py --apply создаёт symlinks:

  • Animation (не R/18+) → /media/cartoons/ или /media/cartoons-series/
  • Documentary → /media/documentaries/ или /media/documentaries-series/
  • Прочее → /media/movies/ или /media/series/

Копирует NFO + постер рядом с symlink (Jellyfin не видит метадату через symlink напрямую).

watchlist-sync — Python CLI

Репо: ~/Developer/watchlist-sync (GitHub: mallexxx/watchlist-sync).

watchlist-sync resolve   — TMDB + KP API → ID, тип (movie/tv)
watchlist-sync sync-down — добавить батч (max 5) в Radarr/Sonarr, поставить ⬇️
watchlist-sync sync-up   — Jellyfin watched → [x] в .md

Резолвер: TMDB /search/multi + KP fallback. Пороги: min_rating: 6.0, min_score: 0.75. При 2+ кандидатах → ❓ (LLM разрешает в cron каждые 6ч, нерешённые → уточнение в Zulip).

Stage 2 (после ≥50 просмотров): AI-curated recommendations — три списка (с женой / один / семья) на основе TMDB профиля, добавляются в конец watchlist как ## 🤖 Рекомендации (YYYY-WNN).

Jellyfin library mapping: Movies → movies, Cartoons → mixed (movies + episodes).

Питфоллы

Фильмы из Kinopoisk с синтаксисом [Title](https://kinopoisk.ru/film/12345/) (2025) передавались как есть в TMDB search → не находились → помечались ❓ навсегда (not i.ambiguous пропускает при следующем запуске). Фикс: commit 2db1ed9 — извлекать plain title из markdown link до передачи в TMDB. Ошибочно помеченные ❓ требуют ручного --recheck.

KP API Endpoint

api.kinopoisk.dev редиректит на api.poiskkino.dev/v1.4/movie. Использовать destination redirect напрямую.

🔴 Жёстко зашитый qualityProfileId в arr.py (корень жирных remux) — РЕШЕНО

Найдено 2026-09-04. В watchlist_sync/arr.py изначально был захардкожен qualityProfileId: 1 и для Radarr, и для Sonarr:

# radarr_add (~стр. 77) / sonarr_add (~стр. 112)
"qualityProfileId": 1,

Профиль 1 (Radarr) = 'Any' — разрешает ВСЁ вплоть до Remux-2160p и BR-DISK, поэтому каждый новый фильм/сериал попадал в «Any» и Radarr скачивал 60GB remux'ы.

✅ Решение принято и применено 2026-09-04: создан профиль WEB-4K (no remux) → Radarr id 7, Sonarr id 7; arr.py теперь пишет qualityProfileId: 7. Подробности профиля — в arr-stack-kraken, секция «Качество загрузки для НОВЫХ фильмов/сериалов».

✅ Баг накопления кандидатов в mark_ambiguous() (найден и исправлен 2026-09-20)

Кандидаты-под-буллеты под ❓-строками дублировались на каждом прогоне resolve (+3 строки за раз), потому что re.sub снимал только саму ❓-строку, а новые кандидаты вставлялись перед старыми. Паттерн исправлен на захват всего блока (маркер + под-буллеты). Полная механика, две неудачные версии паттерна и чистка 168 дублей — в family/projects/watchlist-automation.

Правило для парсеров watchlist: под-буллет кандидата = отступ ≥4 пробела + буллет + НЕ чекбокс (- [ ]/- [x]). Строки фильмов имеют отступ 2 пробела. Внутри *-группы перенос строки обязателен — иначе regex проглатывает соседние фильмы.

Open Tasks

  • ✅ [2026-09-20] Фикс sync_down.py РЕАЛИЗОВАН — коммит ada47da. resolved.sort() убран, добавлен MIN_RATING = 6.0 с видимым print. Нулевые рейтинги отсекаются (решение Alex: «отсекать»).
  • ✅ [2026-09-20] Приоритет ⭐ (U+2B50) РЕАЛИЗОВАН — коммит 7db9385. ⭐ первыми из всех секций, обходит фильтр MIN_RATING. ⭐ срезается из запроса TMDB (parser._clean через str.replace, НЕ через end-anchored класс). ✭ (U+272D) приоритетом не считается; [x] пропускается; под-буллеты кандидатов не считаются.
  • ✅ [2026-09-20] Реальный прогон с новой логикой выполнен — 19:53, Done, +3 фильма. Батч по порядку файла, ⭐ первым.
  • ✅ [2026-09-20] Крон ВОССТАНОВЛЕН — установлен 21:21 в crontab юзера kraken, бэкап ~/crontab-backup-20260920-212155.txt. nightly 01:00 + discover вс 09:00, логи в /tmp/. Первая проверка в бою — прогон в 01:00.
  • 🟡 [2026-09-20] Логи крона в /tmp/ — теряются при перезагрузке. Перенести в постоянное место (~/logs/ или на диск), если нужна история прогонов.
  • ✅ [2026-09-21] Удаление просмотренных фильмов при заполнении диска >90% — ЗАКРЫТО И В КРОНЕ. Обёртка ~/scripts/watchlist-cleanup.sh (была в /tmp — перенесена через sudo install, у kraken sudo без пароля), cron 0 2 * * * ... --apply, лог /home/kraken/logs/watchlist-cleanup.log. Корзина Radarr (recycleBin) — решено НЕ настраивать (контейнерный/хостовый пути расходятся; Alex больше не переспрашивать). Обёртка по умолчанию делает dry-run — без --apply в кроне был бы вечный холостой ход. Полные детали — family/plans/watchlist-cleanup. ⚠️ Питфолл: /media — не медиа-диск, а пустая папка на SD-карте (/dev/mmcblk0p2), отдаёт 33.9%. Реальный диск /dev/sda1 в /srv/dev-disk-by-uuid-6194539b-.../. Использовать os.statvfs, не shutil.disk_usage("/media").
  • 🔴 [2026-09-21] ДИСК КРИТИЧЕН: 94.7% / свободно 4.8 ГБ. Порог 90% пройден, cleanup реально запускается, но удаляет 0 файлов: пересечение [x] ∩ Radarr-библиотека = пусто (177 просмотренных vs 28 фильмов с файлами). Ждёт, пока в watchlist поставят [x] на Radarr-фильмы. Топ-кандидаты: Babylon 98.8 ГБ, Schindler's List 95.9, Midsommar 91.9, Triangle of Sadness 85.6.
  • 🔴 [2026-09-21] КОРЕНЬ: [x] не проставляется на новые закачки. sync-up (Jellyfin watched → [x]) видит только 6 старых фильмов из /media/movies, ни одного из /media/movies-radarr. Пока это не починено, cleanup бесполезен, а диск будет заполняться. Главная незакрытая задача всего pipeline.
  • 🟡 [2026-09-20] sync-down берёт всего 5 за прогон — при 250 кандидатах разбор очереди займёт ~50 ночей. Рассмотреть увеличение --batch (сейчас --batch 5 в watchlist-nightly.sh). Особенно актуально, раз фильтр <6.0 уже отсеял 18.
  • 🟡 [2026-09-20] Разобрать 5 застрявших закачек в очереди Radarr со статусом warning: Brokeback Mountain, Мортал Комбат 2, Sorry to Bother You, Город бога, День хомячка. Висят давно, релиз не находится/не качается.
  • 🟡 [2026-09-20] Oficina do Diabo / Мастерская Сатаны (2025) — в Radarr, но релиз не найден. Ждёт RSS. Проверить через несколько дней.
  • 🟡 [2026-09-20] Настроить Zulip-пинг — ZULIP_URL / ZULIP_API_KEY / ZULIP_BOT_EMAIL отсутствуют в .env watchlist-sync, 59 нерезолвленных items ушли в никуда без уведомления.
  • 🟡 [2026-09-20] Добавить pre-check дублей в sonarr_add() — сейчас при уже существующей серии прилетает 400 SeriesExistsValidator и шумит в логе. В radarr_add() проверка есть.
  • 🟡 [2026-09-20] Синхронизировать документацию с кодом — watchlist-sync §1.2 и §1.3 описывают порог min_rating: 6.0 и «приоритет = порядок в файле», которых в коде не было. Порог теперь реализован (MIN_RATING), приоритет по порядку тоже, плюс появился приоритет ⭐ — обновить формулировки в watchlist-sync.
  • 🔴 [2026-09-04] Решить стратегию качества загрузки новых фильмов — уже РЕШЕНО 2026-09-04 (профиль WEB-4K (no remux), id 7 для Radarr и Sonarr). Осталось только убедиться, что старые ⬇️-позиции, добавленные до фикса, не тянут remux.
  • 🟡 Оставшиеся ❓-позиции (71 маркер) — намеренно не чистятся вручную (решение Alex 2026-09-20). Разбираются через resolve --recheck при работающем кроне.
  • 🟡 Передавать Transmission locations через router script при каждом импорте (сейчас вручную)
  • 🔴 rsync невыполненных фильмов с HTPC на Кракен (Фильмы не перенесенные с htpc на кракен.md)
  • 🟡 Watchlist sync-up: добавить перенесённые фильмы в нужные разделы

Связанные заметки