52 KiB
created, updated, status, tags
| created | updated | status | tags | ||||
|---|---|---|---|---|---|---|---|
| 2026-05-20 | 2026-09-21 | active |
|
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→activegrep -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
)
⚠️ Две НЕУДАЧНЫЕ версии паттерна (не повторять)
- v1 —
(?:\n[ \t]+-[^\n]*\n?)*:\n?опционален, поэтому*перешагивал на следующую строку без разделителя и проглатывал соседние фильмы. Потеряны❓ The kill room,The burial,Past lives. Откачено. - v2 — то же плюс негативный lookahead: не помогло,
[^\n]*из предыдущей итерации уже захватил хвост следующей строки. - 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 alley33→3,Stand by me30→3,Фарго30→3,Left behind21→4,Крестный отец12→3,Godfathers12→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— фильтр только для шага discoverresolver.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 формулировал требования короткими репликами, и план сначала был неверно истолкован дважды:
- «⭐ - в приоритете, из всех списков. Естественно в tmdb ее срезать» — уточнение, что секции не ограничены и глиф не должен уходить в запрос.
- «Что блядь значит рейтинг остался?! Я блядь сказал в приоритете завела» — категорическое: ⭐ обходит фильтр рейтинга, а не просто меняет очерёдность.
Вывод для будущего: когда 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 (на Кракене):
sync-vault.sh(pull) →process-thumbs --apply→sync-vault.sh(push)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).
Питфоллы
KP Markdown Link Parsing Bug
Фильмы из 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.nightly01: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 без пароля), cron0 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отсутствуют в.envwatchlist-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: добавить перенесённые фильмы в нужные разделы
Связанные заметки
- watchlist-discover — шаг discover: KP API, format, CLI команды
- watchlist-sync — основной проект (resolve, sync-down, sync-up)
- arr-stack-kraken — детальные питфоллы *arr стека
- jellyfin-transcode-rpi5 — Jellyfin PGS/ASS питфоллы