diff --git a/family/projects/watchlist-automation.md b/family/projects/watchlist-automation.md index 225ca66d..c8380193 100644 --- a/family/projects/watchlist-automation.md +++ b/family/projects/watchlist-automation.md @@ -1,6 +1,6 @@ --- created: '2026-05-20' -updated: '2026-09-21' +updated: '2026-09-22' status: active tags: - kraken @@ -50,6 +50,12 @@ Jellyfin (8096) → rescan → виден контент | `🧟‍♀️` | horror (genre_id=27) | **не скачивается** (фильтр в `sync-down` с 2026-09-21), если нет `⭐` | | `⭐` | **приоритет к загрузке** (U+2B50) | sync-down берёт в первую очередь, **обходит ВСЕ фильтры** (рейтинг + ужасы) ✅ 2026-09-20/21 | +> 🔜 **С 2026-09-22 список читается ещё и в UI Lampa на ТВ** — непросмотренные `[ ]` отдаются +> каталогом «Мой список» через модуль `Catalog` в Lampac. ⭐ первыми, рейтинг ↓, ужасы последними. +> План: [[personal/plans/lampac-watchlist-plugin]]. +> ⚠️ Отсюда требование: **`## Новинки` не должна содержать строк, чей фильм уже `[x]`** — иначе дубль +> в каталоге. Чистится автоматически `process-thumbs` (фикс 2026-09-22, см. ниже). + ⚠️ **`✭` (U+272D) — НЕ приоритет.** Alex 2026-09-20 явно указал: звёздочкой-приоритетом считается только `⭐` (U+2B50). Символ `✭` используется в файле как обычная пометка «выделенное», приоритетом не является. Также: `⭐` засчитывается **только в строке фильма** (`- [ ] Название ⭐ …`), НЕ в под-буллетах кандидатов, которые генерирует `mark_ambiguous` (там формат «Название (год) ★N.N»). `[x]` (просмотренные) с `⭐` игнорируются. Работает **из всех секций** (`## Фильмы`, `## Новинки`, `## Мультфильмы`, `## Аниме`). @@ -125,6 +131,859 @@ crontab -l # проверка ⚠️ **В `docker exec hermes-kraken hermes cron list` по-прежнему «No scheduled jobs»** — это НЕ про watchlist. Watchlist-скрипты детерминированные, живут в системном cron, в Hermes-планировщике их не должно быть. Не искать их там. +## 🔴 2026-09-22: ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — баг и фикс + +**Симптом (сообщил Alex):** «Проект Конец света» показывался в каталоге Lampa как непросмотренный, +хотя в главном списке он `[x]` (просмотрен) и был скачан. + +**Корень — `process_thumbs()` в `parser.py`.** + +Функция обрабатывает только три случая по регекспу +`_THUMBS_RE = r'^(\s*-\s*\[.\]\s*)(👍|👎)?\s*(.*?)\s*(👍|👎)? + +**Симптом:** диск `/dev/sda1` на Kraken — **0 байт свободно** (`df` 100%). Radarr API отдавал +`disk I/O error` (SQLite не может писать при нуле места) — метаданные недоступны. + +**Причина (НЕ cleanup):** `sync-down` в кроне `watchlist-nightly` (01:00) добавлял +**5 фильмов ежедневно** (`--batch 5`), **без проверки свободного места** и **без фильтра ужасов**. +Radarr качал remux'ы по 57–93 ГБ, пока диск не кончился. Профиль `WEB-4K (no remux)` фильтрует +**тип релиза**, но НЕ размер файла и НЕ проверяет место — от переполнения не защищает. + +**Освобождено вручную:** удалены 5 remux = **403 ГБ** (Babylon 93 / Schindler's List 90 / +Midsommar 86 / Triangle of Sadness 80 / Unhinged 57 ГБ), всем выставлен `monitored=false` +через Radarr API. Диск: **100% → 77%** (404 ГБ). + +**Починено в коде (коммиты `2642882`, `a131208`):** + +| Что | Деталь | +|---|---| +| Стоп-кран по месту | `MIN_FREE_GB = 100.0` в `sync_down.py`, проверка через `shutil.disk_usage` (не `df`) | +| Фильтр ужасов | `candidate.is_horror()` (genre_id=27) пропускает ужасы, если нет `⭐` | +| `mark_queued` | возвращает `bool`, поглощает эмодзи-префикс, логирует промах через `_mark()` | +| `unmark_queued` | **новая функция** — снятие `⬇️` (не существовала) | +| `_clean` | чистит **множественные** эмодзи (`^(?:[chars]+\s*)+`) и **одиночный** год (`*` вместо `+`) | +| `sync_up._mark_watched` | видит эмодзи-префикс (иначе фильм не переходил в `[x]`) | +| `--batch` | **5 → 2** в `~/scripts/watchlist-nightly.sh` (root:root, править `sudo cp`) | + +**Питфоллы, найденные в процессе:** +- `~/scripts/` на Kraken — **root:root 755**, `scp` падает с `Permission denied`. Только `sudo cp`. +- Крон-файлы вернуть можно только через `crontab /tmp/файл`; `crontab -l > f` для бэкапа обязателен. +- `du` по USB-HDD не влезает в SSH-таймаут на 100%-заполненном диске — запускать в фоне через `systemd-run`. +- Когда диск в нуле, **сначала освободить место**, только потом любые API-вызовы Radarr: SQLite мёртв. + +**Открытая задача:** `cleanup` ([[family/plans/watchlist-cleanup]]) **не покрывает `/media/movies`** — +смотрит только Radarr-библиотеку, поэтому просмотренное из ручных торрентов не удаляется. +Пересечение `[x]` ∩ Radarr-файлы = 0. + +**Полный разбор инцидента:** [[family/plans/watchlist-disk-overflow-fix]] + +## 🔴 Аудит 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()`: + +```python +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 строки оставались **ниже** — реплейс не знал о них. Итог: каждый прогон добавляет ещё один блок кандидатов поверх существующих. + +**Важно:** `❓`-строки НЕ дублировались (каждая — одна). Дублировались **под-буллеты** под ними. + +### Фикс +Паттерн теперь захватывает **весь блок**: строку-маркер ПЛЮС её под-буллеты, и перезаписывает целиком. + +```python +# 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' + _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`: +```python +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`: +```python +# 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 +``` +```python +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 прощал лишний глиф). + +```python +# ⭐ (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`, двухпроходная выборка:** + +```python +# ⭐ (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` пустой). + +## 🔴 2026-09-21: ДИСК ЗАПОЛНЕН 100%, КРОН `nightly`/`discover` ОСТАНОВЛЕН + +**Причина остановки:** `sync-down --batch 5` (5 фильмов ежедневно) работал **без проверки свободного места**. Диск заполнился до **нуля байт**. + +### Признак «сервисы ломаются» — Radarr отдаёт disk I/O error + +```bash +curl -s "http://localhost:7878/radarr/api/v3/movie?apikey=$KEY" +# → {"message": "disk I/O error\ndisk I/O error", +# "description": "...System.Data.SQLite.SQLiteException (0x87AF045D): disk I/O error..."} +``` +SQLite не может писать при нуле свободного места. **Метаданные Radarr через API не читаются, пока диск не освобождён.** Это первое, что надо проверять при «Radarr молчит» на заполненном диске. + +### Фактическое заполнение (2026-09-21) + +``` +df -h → /dev/sda1 1.8T 1.7T 0 100% /srv/dev-disk-by-uuid-6194539b-... +df -h → /dev/mmcblk0p2 14G 4.7G 8.4G 36% / (SD-карта, не медиа-диск) +``` +Docker: 29.4.3, Debian 12 bookworm, **arch `aarch64`**, 25 контейнеров, 22 образа. + +### Что именно съело место — 5 REMUX-файлов = 403.4 ГБ + +Замер `find -printf` по `/media/movies-radarr` (27 видеофайлов). **Настоящих remux'ов ровно 5** — по имени файла (`Remux`/`BDREMUX`/`BDRemux`): + +| Размер | Фильм | Файл | +|---|---|---| +| 92.0 GB | Babylon (2022) | `Babylon (2022) - UHD BD-Remux by Wild_Cat.mkv` | +| 89.4 GB | Schindler's List (1993) | `Schindler's.List.1993.2160p.HDR.BDRemux.mkv` | +| 85.6 GB | Midsommar (2019) | `Midsommar.2019.DC.BDREMUX.2160p.HDR.seleZen.mkv` | +| 79.7 GB | Triangle of Sadness (2022) | `Triangle.of.Sadness.2022.BDREMUX.2160p.HDR.seleZen.mkv` | +| 56.7 GB | Unhinged (2020) | `Неистовый.2020.UHD.Blu-Ray.Remux.2160p.mkv` | +| **403.4 GB** | **итого** | | + +⚠️ **Остальные 22 файла — НЕ remux** (WEB-DL / BDRip / x264 / x265, 4–29 ГБ). Крупные, но кодированные — не трогать. Например `Oldboy (2003)/Oldeuboi [2003].mkv` 28.6 ГБ и `Project.Hail.Mary...2160p.iT.WEB-DL.DV.HDR.H.265.mkv` 28.1 ГБ — **не remux**, несмотря на размер. + +**Корень:** захардкоженный `qualityProfileId: 1` (профиль `Any`) в `arr.py` — починен только 2026-09-04. Всё, что качалось до этого, брало remux'ы по 60–98 ГБ. + +### ✅ ИСПРАВЛЕНО 2026-09-21: ВСЕ 5 фильмов ЕСТЬ в watchlist + +> ⚠️ **Ранее в этой доке стояло ошибочное «Schindler's List не найден в файле».** Это был **ложный вывод +> из одностороннего грепа**: фильм записан **по-русски** — «Список Шиндлера» (строка 396), +> а искали по латинице. Alex поправил. +> **Правило: искать и ru, и en — `grep -i "schindler\|шиндлер\|список"` — ПРЕЖДЕ чем утверждать «нет в списке».** + +| Фильм | Строка в watchlist | Метка `⬇️` | +|---|---|---| +| Неистовый (Unhinged) | **107**: `- [x] ⬇️ Неистовый` | ✅ есть (и `[x]`) | +| Список Шиндлера | **396**: `- [ ] ⬇️ Список Шиндлера` | ✅ есть | +| Babylon (2022) | **83**: `- [ ] Babylon (2022)` | ❌ **потеряна** (баг `mark_queued`) | +| Midsommar | **81**: `- [ ] 🧟‍♀️ midsommar 2019` | ❌ **потеряна** | +| Triangle of Sadness | **82**: `- [ ] Triangle of Sadness 2022` | ❌ **потеряна** | + +**Корень потери 3 меток — баг `mark_queued` (`parser.py:77`):** regex требует название **сразу** после `- [ ]`, +но перед ним бывает эмодзи (`- [ ] 🧟‍♀️ midsommar 2019`) → не матчит; при провале функция **молча ничего не пишет** +(`if new_text != text`), а `sync_down.py` её результат **не проверяет**. Детали — [[family/plans/watchlist-disk-overflow-fix]], Фаза 1. + +### ❌ «Восстановить потерянные `⬇️`» — отменено (неверная постановка) + +`⬇️` = «добавлен в Radarr / качается». После удаления файлов метка должна быть **снята**, а не восстановлена. +У Babylon/Midsommar/Triangle метки и не было → **ничего не делать** (консистентно: файла нет, метки нет). +Снять `⬇️` нужно у **Списка Шиндлера** (396 → `[ ]`) и **Неистового** (107 → `[x]`, `[x]` не трогать). +⚠️ Функции `unmark_queued()` в коде **НЕТ** — писать в рамках Фазы 1. + +### ✅ Порядок работ — Фазы 0 и 0b ВЫПОЛНЕНЫ (2026-09-21) + +1. ✅ **Удалены 5 remux-файлов** (403 ГБ) → `df` 100% → **77%**, свободно 404 ГБ → Radarr API ожил (49 фильмов). + Удаление — **файлами напрямую** (`rm` по точным путям), не через Radarr API (тот лежал). Папки с `.nfo`/постерами не тронуты. +2. ✅ **Radarr: `RescanMovie` + снятие `monitored`** у id 4/18/35/36/37 → все `monitored=false`, `hasFile=false`. + ⚠️ **Без этого Radarr перекачал бы все 403 ГБ.** Порядок: `monitored=false` через PUT **до** любого поиска. +3. ✅ **Снят `⬇️`** у Шиндлера (396) и Неистового (107) — через новую `unmark_queued()`. +4. ✅ **`MIN_FREE_GB = 100.0` в `sync-down`** — согласован с Alex («Да»), коммит `2642882`. +5. ✅ Лимит качества Alex считает уже настроенным (`WEB-4K (no remux)`, id 7) — новый лимит не делали. + +**Бэкапы:** `~/backup-movies-watchlist-20260921-150759.md`, `~/backup-radarr-unmonitor-20260921-151117.json`, +`~/crontab-backup-20260921.txt`. Скрипты: `/tmp/rm_remux.sh`, `/tmp/unmonitor_remux.sh`. + +### Остановка крона (сделано 2026-09-21) + +```bash +crontab -l > ~/crontab-backup-$(date +%Y%m%d-%H%M%S).txt # бэкап ПЕРЕД +crontab -l | grep -v "watchlist-nightly\|watchlist-discover" > /tmp/crontab-new.txt +crontab /tmp/crontab-new.txt +crontab -l | grep -c "watchlist-nightly\|watchlist-discover" # → 0 (проверка) +``` +Бэкап: `~/crontab-backup-20260921.txt`. +**Осталось в кроне:** `sync-vault` (часовой), 2× `pgs-to-srt`, `watchlist-cleanup` (02:00). +**Убрано:** `watchlist-nightly` (01:00), `watchlist-discover` (вс 09:00). + +### 🔴 Незакрытые дефекты скрипта (моя ответственность) + +| # | Дефект | Следствие | Статус | +|---|---|---|---| +| 1 | Нет проверки свободного места перед `sync-down` | Radarr качает, пока диск не кончится | ✅ исправлено 2026-09-21 | +| 2 | Нет лимита размера файла | Один фильм = 60–100 ГБ | ⚠️ Alex считает лимит качества достаточным — не делаем | +| 3 | Нет стоп-крана при заполнении > порога | Нет автоматической остановки | ✅ = п.1 | +| 4 | `cleanup` не видит `/media/movies` | Просмотренное не удаляется, диск не освобождается | ⏸ отдельная задача | +| 5 | `mark_queued` молча проваливается | часть фильмов без метки `⬇️` | ✅ исправлено 2026-09-21 | + +**Правило на будущее:** любой скрипт, ставящий закачки в очередь, ОБЯЗАН проверять свободное место **до** добавления. Лимита качества недостаточно — он ограничивает тип релиза, но не размер. + +--- + +## ✅ 2026-09-21 (продолжение): Фазы 1–4 РЕМОНТА ВЫПОЛНЕНЫ — коммит `2642882` + +> Полный план и разбор — [[family/plans/watchlist-disk-overflow-fix]]. +> Ниже — операционная выжимка для будущих сессий. + +**Коммит:** `2642882 fix(sync-down): MIN_FREE_GB guard, horror filter (star overrides), reliable mark_queued` +Предыдущий HEAD — `aafbaf7`. Рабочее дерево чистое. + +### Фаза 1 — `MIN_FREE_GB` (стоп-кран по месту) + +```python +MIN_FREE_GB = 100.0 + +def _free_gb(path: Path) -> float: + p = Path(path).resolve() + while not p.exists() and p != p.parent: + p = p.parent + return shutil.disk_usage(p).free / (1024 ** 3) +``` + +Проверка стоит в `run()` сразу после получения Jellyfin-user, **перед** `parse()`: +при нехватке места печатает `[STOP] free space ... < 100 GiB — nothing queued` и `return`. +Путь берётся из env `WATCHLIST_MEDIA_ROOT`, иначе `watchlist.parent`. + +⚠️ **`shutil.disk_usage`, НЕ `df`** — `df` врёт на ~4% (учитывает root-резерв ext4). +⚠️ Питфолл из [[watchlist-cleanup]]: `/media` — **не медиа-диск**, а пустая папка на SD-карте. Учитывать при задании `WATCHLIST_MEDIA_ROOT`. + +### Фаза 2 — Фильтр ужасов + `⭐ > 🧟` + +Расширен существующий фильтр (был только рейтинговый), точка — бывший `sync_down.py:54`: + +| Условие | Действие | +|---|---| +| есть `⭐` (U+2B50) | качаем **всегда** — `continue` до всех фильтров | +| `candidate.is_horror()` без `⭐` | **пропускаем** | +| рейтинг < `MIN_RATING` (6.0) без `⭐` | пропускаем | + +Счётчик `skipped_horror`, в конце лога: `(N horror item(s) skipped; add ⭐ to force)`. + +⚠️ **`is_horror()` работает по жанрам TMDB, не по эмодзи.** `genres` — список int-id, `27` = Horror. +Поэтому отсекается **33** (dry-run), хотя меток `🧟♀️` в файле 41 — часть строк не резолвится в TMDB. +В `.resolve_cache.json` **51 запись содержит genre id 27**. Проверка: +```bash +jq -r "[to_entries[] | select((.value.genres // .value.candidate.genres // []) | index(27))] | length" .resolve_cache.json +``` + +⚠️ `⭐` (U+2B50) ≠ `✭` (U+272D). Приоритет — только `⭐`. + +**Dry-run подтверждён:** +``` +free space on /home/kraken/obsidian/family/documents: 403.4 GiB (threshold 100 GiB) + ⭐ 1 priority item(s) ['Простите за беспокойство 2018'] + (14 item(s) below rating 6.0 skipped) + (33 horror item(s) skipped; add ⭐ to force) +Queued 5/5 items [dry-run] +``` + +### Фаза 3 — Надёжный `mark_queued` + +`parser.py::mark_queued()` переписан: +- **возвращает `bool`** +- регексп поглощает **опциональный эмодзи-префикс** между чекбоксом и названием + (`🧟 🗓️ ❤ ⭐ ✭ ❓ ✅ ⬇️ ▶️ ZWJ VS16 ♀️` + пробелы) +- метка пишется **перед** префиксом: `- [ ] ⬇️ 🧟♀️ midsommar 2019` +- в `sync_down.py` — обёртка `_mark()` (4 места вызова), при `False` печатает + `[!] could not write ⬇️ marker for ... (line not matched — check the watchlist syntax)` + +**Тесты:** 7/7. Проверка на копии боевого файла: строки 81/82/83 получили метку, 677 строк → 677. + +### Фаза 4 — `unmark_queued()` добавлена и применена + +`unmark_queued()` в `parser.py` — инверсия `mark_queued`, тоже возвращает `bool`, поглощает эмодзи-префикс +с обеих сторон маркера. Применено на боевом файле: +``` +107: - [x] ⬇️ Неистовый → - [x] Неистовый +396: - [ ] ⬇️ Список Шиндлера → - [ ] Список Шиндлера +``` +**Тесты:** unmark 5/5 + round-trip `mark → unmark → исходная строка` ✅ + +⚠️ **Не «восстанавливать» `⬇️` после удаления файла** — метка = «добавлен в Radarr»; после +удаления + `monitored=false` она врёт и блокировала бы повторную постановку. + +### 🔴 Открытые баги в `parser.py::_clean()` — НЕ исправлены + +**Существовали до правок 2026-09-21**, проверены на старом коде — не регрессия. Решение Alex не принято. + +| # | Баг | Строка | Эффект | +|---|---|---|---| +| **A** | `re.sub(r'\s*\(\d{4}(?:/\d{4})+\)\s*$', ...)` — квантификатор `+` требует **2+ варианта года**, одиночный год не срезается | `parser.py:47` | `_clean('⬇️ Babylon (2022)')` → `'Babylon (2022)'` — год уходит в `resolve()` (запрос в TMDB) и `mark_queued` | +| **B** | `_EMOJI_PREFIX` = `^[<символы>]+\s*` заканчивается на `\s*` → захватывает **только первый** эмодзи | `parser.py:19-38` | `'⬇️ 🧟♀️ midsommar 2019'` → `'🧟♀️ midsommar 2019'` (эмодзи остаётся в `title`). Лечится формой `^(?:[<символы>]+\s*)+` | + +**Почему важно:** грязный `title` идёт в `resolve()` (TMDB/KP), в `_normalize()` при `sync_up` +(не сойдётся с названием из Jellyfin) и в `mark_queued`. Фикс B **частично компенсирован** +правкой Фазы 3 (регексп поглощает префикс), но `title` всё равно уходит в `resolve` грязным. + +### Фаза 3.5 — `_mark_watched` в `sync_up.py` (исправлено локально, НЕ залито/НЕ закоммичено) + +Тот же класс бага: регексп `r'\s*-\s*\[ \]\s*⬇️?\s*(.*)'` не пропускал эмодзи-префикс, поэтому +строка `- [ ] ⬇️ 🧟♀️ midsommar 2019` **никогда не перешла бы в `[x]`**. Исправлено симметрично +(`/tmp/wlsync/sync_up.py`), тесты 5/6 (падение на `Babylon (2022)` — баг A, не от правки). + +⚠️ **На Kraken НЕ залито, в git НЕ закоммичено.** На момент правки строк с `⬇️` + эмодзи-префиксом +в файле **не было ни одной** — баг проявился бы **после первого боевого прогона** `sync-down` +(правка Фазы 3 создаёт именно такой формат). Чинить синхронно. + +### Бэкапы этой сессии +``` +~/backup-movies-watchlist-20260921-150759.md (перед удалением remux) +~/backup-radarr-unmonitor-20260921-151117.json (состояние Radarr до unmonitor) +~/backup-wlsync-20260921-151855/ (sync_down.py + parser.py до правок) +~/backup-movies-watchlist-unmark-20260921-152018.md (перед снятием ⬇️) +~/crontab-backup-20260921.txt (crontab до остановки) +``` + +### Скрипты сессии (на Кракене `/tmp/`, теряются при перезагрузке) +`rm_remux.sh` (удаление 5 remux), `unmonitor_remux.sh` (снятие monitored + RescanMovie), +`commit_wlsync.sh`, `test_marker_real.sh` (тест mark_queued на копии боевого файла), +`fase4_unmark.sh` (снятие ⬇️). + +--- + +## Cron Jobs (на Кракене, восстановлены 2026-09-20, дополнены 2026-09-21) + +Весь watchlist pipeline работает на Кракене (мигрировал с Eagle 2026-05-27). **Крон был мёртв с 2026-06-20 по 2026-09-20, восстановлен** (детали в начале документа). + +> ⚠️ **С 2026-09-21 `watchlist-nightly` и `watchlist-discover` ОСТАНОВЛЕНЫ** (диск 100%, см. секцию выше). Таблица ниже — целевое состояние; фактически активен только `watchlist-cleanup`. + +| Job | Расписание | Скрипт | Факт 2026-09-21 | +|-----|-----------|--------|-----------------| +| `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 вручную (рабочая команда) +```bash +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: +```python +# 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] ДИСК ОСВОБОЖДЁН: 100% → 77%, свободно 404 ГБ.** Удалены 5 remux-файлов (403 ГБ); в Radarr снят `monitored` у id 4/18/35/36/37 (`hasFile=false`) — иначе он перекачал бы всё заново. Radarr API ожил (49 фильмов). Бэкапы: `~/backup-movies-watchlist-20260921-150759.md`, `~/backup-radarr-unmonitor-20260921-151117.json`. +- ✅ **[2026-09-21] `watchlist-nightly` + `watchlist-discover` ОСТАНОВЛЕНЫ, ЗАТЕМ ВОЗВРАЩЕНЫ.** Останавливали при диске 100% (`sync-down --batch 5` без проверки места). **Возвращены в тот же день** после Фаз 1–4, уже с **`--batch 2`**. Бэкапы crontab: `~/crontab-backup-20260921.txt` (до), `~/crontab-backup-20260921-152741.txt` (после). Итоговый crontab: + ``` + 0 1 * * * watchlist-nightly.sh # batch 2 + 0 9 * * 0 watchlist-discover.sh + 0 2 * * * watchlist-cleanup.sh --apply + ``` +- ✅ **[2026-09-21] ФАЗА 1 РЕМОНТА ВЫПОЛНЕНА: `MIN_FREE_GB = 100.0` в `sync-down`** — коммит `2642882`. Проверка через `shutil.disk_usage` (НЕ `df`, врёт на ~4%), путь из env `WATCHLIST_MEDIA_ROOT` иначе `watchlist.parent`. Печатает `[STOP] free space ... < 100 GiB — nothing queued` и выходит. Подтверждено dry-run: `403.4 GiB (threshold 100 GiB)`. Полный план — [[family/plans/watchlist-disk-overflow-fix]]. +- ✅ **[2026-09-21] ФАЗА 2 ВЫПОЛНЕНА: фильтр ужасов + `⭐ > 🧟`** — коммит `2642882`. Точка — бывший `sync_down.py:54`. `⭐` обходит **все** фильтры (`continue` до проверок); ужас без `⭐` пропускается; рейтинг < 6.0 пропускается. Dry-run: **33 horror skipped**, 14 по рейтингу. ⚠️ `is_horror()` считает по жанрам TMDB (id 27), не по эмодзи — поэтому 33, а не 41. +- ✅ **[2026-09-21] ФАЗА 3 ВЫПОЛНЕНА: надёжный `mark_queued`** — коммит `2642882`. Возвращает `bool`, поглощает эмодзи-префикс (`- [ ] ⬇️ 🧟♀️ Title`), обёртка `_mark()` в `sync_down.py` логирует провал `[!]`. Тесты 7/7 + проверка на копии боевого файла. **Корень потери 3 меток `⬇️`.** +- ✅ **[2026-09-21] ФАЗА 4 ВЫПОЛНЕНА: `unmark_queued()` добавлена, `⬇️` снят** — у Шиндлера (396) и Неистового (107). Функции в коде **не было**. Тесты unmark 5/5 + round-trip. Бэкап `~/backup-movies-watchlist-unmark-20260921-152018.md`. +- ✅ **[2026-09-22] ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — ИСПРАВЛЕНО.** Симптом Alex: «Проект Конец света» в каталоге Lampa как непросмотренный, хотя в главном списке он `[x]`. Корень: `process_thumbs` обрабатывает только `👍`/`👎`/`[x]`; строка `- [ ] ⬇️ [Название](kp) (2025) IMDb 7.1` (без оценки) уходила в `keep` **навсегда**, а при наличии `[x]`-двойника давала дубль в каталоге. **Фикс:** сбор `watched_titles`/`watched_kp` по всем секциям кроме Новинок + удаление сирот из `keep`; нормализация **обязана срезать год и рейтинги**, иначе `проектконецсвета` ≠ `проектконецсвета2026imdb84kp84`. Флаг `_patch_dropped` добавлен в ранний выход (иначе запись файла пропускалась). `cli.py` — явные ветки вывода `👍/👎/[x]/drop`. Проверено: удаляется ровно 1 строка, 683 → 682; каталог 404 → 403. Бэкапы: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`, код — `parser.py.bak-orphan-*`, `.bak-guard-*`, `.bak-bs-*`, `cli.py.bak-cli-*`. Полный разбор — секция «ОСИРОТЕВШИЕ СТРОКИ» выше; связь с каталогом Lampa — [[personal/plans/lampac-watchlist-plugin]]. +- 🔴 **[2026-09-22] ПИТФОЛЛ правки `parser.py` скриптами: двойное экранирование съедает слэши.** `r'\1'` → реальный байт `\x01`, `r'\b'` → байт `\x08` (backspace). `compile()`/`ast.parse()` проходят — синтаксис валиден, а регекспы **молча не работают** (нормализация не срезала рейтинги, сироты не находились). **Правило: после записи патча проверять файл на `chr(1)` и `chr(8)`, а не только на синтаксис.** По возможности править через `patch`/Obsidian, а не генератором строк. +- 🔴 **[2026-09-21] ФАЗА 5: ВЕРНУТЬ КРОН — ждёт.** `watchlist-nightly` (01:00) + `watchlist-discover` (вс 09:00) остановлены, бэкап `~/crontab-backup-20260921.txt`. Возвращать с **`--batch 2`** (не 5) и **только после проверки Фаз 1–2 в бою** (сначала dry-run). +- 🔴 **[2026-09-21] ОТКРЫТЫЕ БАГИ `_clean()` — решение Alex НЕ принято.** Баг **A** (`parser.py:47`): `(?:/\d{4})+` требует 2+ варианта года → одиночный `(2022)` не срезается, уходит в `resolve()`. Баг **B** (`parser.py:19-38`): `_EMOJI_PREFIX` заканчивается на `\s*` → берёт только первый эмодзи, `🧟♀️` остаётся в `title`. Оба **существовали до правок** (проверено на старом коде), не регрессия. Лечение B — форма `^(?:[<символы>]+\s*)+`. +- 🔴 **[2026-09-21] `_mark_watched` в `sync_up.py` — исправлено локально, НЕ залито/НЕ закоммичено.** Тот же класс бага: строка `- [ ] ⬇️ 🧟♀️ Title` не перешла бы в `[x]`. ⚠️ Правка Фазы 3 **создаёт** именно такой формат → чинить синхронно с возвратом крона, иначе `sync-up` перестанет видеть новые закачки. +- 🔴 **[2026-09-21] КОРЕНЬ: `[x]` не проставляется на новые закачки.** `sync-up` (Jellyfin watched → `[x]`) видит только 6 старых фильмов из `/media/movies`, ни одного из `/media/movies-radarr`. Пока это не починено, cleanup бесполезен, а диск будет заполняться. +- 🟡 **[2026-09-21] Cleanup не видит `/media/movies`** — смотрит только Radarr-библиотеку, поэтому просмотренное из папки ручных торрентов не удаляется. Расширять — отдельно и осторожно (удаление файлов). +- 🟡 **[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: добавить перенесённые фильмы в нужные разделы + +## Связанные заметки + +- [[watchlist-discover]] — шаг discover: KP API, format, CLI команды +- [[watchlist-sync]] — основной проект (resolve, sync-down, sync-up) +- [[arr-stack-kraken]] — детальные питфоллы *arr стека +- [[jellyfin-transcode-rpi5]] — Jellyfin PGS/ASS питфоллы +`: + +| Строка | Что делает | +|---|---| +| `👍` | копирует в начало главного списка, из Новинок **удаляет** | +| `👎` | переносит в `## Новинки👎` | +| `[x]` | переносит в конец `[x]` главного списка | +| **всё остальное** | **остаётся в `keep` = навсегда в `## Новинки`** | + +Строка вида `- [ ] ⬇️ [Название](kp) (2025) IMDb 7.1` **не имеет ни 👍, ни 👎** → попадает в `keep`. +Если тот же фильм уже `[x]` в главном списке — он висит в Новинках как непросмотренный. +В каталоге Lampa это **дубль**. + +**Фикс — два блока в `parser.py`, оба в `process_thumbs()`:** + +1. **Сбор просмотренного** — обход всех строк файла, исключая `## Новинки` и `## Новинки👎`: + ```python + watched_titles = set() # нормализованные названия [x]-строк + watched_kp = set() # KP-id из [x]-строк + ``` + Нормализация (`_norm_title`) **обязана срезать год и рейтинги** — иначе ключи не совпадут: + ```python + s = re.sub(r'\[([^\]]+)\]\([^)]*\)', r'\1', s) # ссылка -> текст + s = re.sub(r'\(\s*\d{4}(?:\s*/\s*\d{4})*\s*\)', ' ', s) # (2025), (2006/2020) + s = re.sub(r'\b(?:IMDb|KP)\s*[\d.,]+', ' ', s, flags=re.I) # IMDb 8.4 KP 8.4 + s = re.sub(r'[^0-9a-zа-яё]+', '', s.lower(), flags=re.I) + ``` + ⚠️ Без среза года/рейтингов в главном списке ключ `проектконецсвета`, а в Новинках + `проектконецсвета2026imdb84kp84` — **не совпадают**. + +2. **Удаление сирот из `keep`** — по `watched_kp` (точное) или `watched_titles` (нормализованное), + с записью в `actions` как `('[drop] уже просмотрено в другой секции', ...)`. + +3. **Флаг `_patch_dropped`** — ранний выход `if not thumbs_up and not thumbs_down and not watched_items: return []` + **пропускал запись файла**, даже когда сироты найдены. Добавлено `and not _patch_dropped`. + +**`cli.py`:** вывод `[drop]` был неотличим от `👎` (обобщённый `else`) → явные ветки +`👍 / 👎 / [x] / drop`. + +**Проверка (2026-09-22):** на копии боевого файла — удаляется **ровно 1 строка**, 683 → 682, +«Конец света» пропадает из всех 4 категорий каталога (404 → 403 позиции). + +Бэкап до правки: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`. +Бэкапы кода: `parser.py.bak-orphan-*`, `.bak-guard-*`, `.bak-bs-*`; `cli.py.bak-cli-*`. + +### 🔴 Питфолл при правке `parser.py` скриптами + +**Двойное экранирование в patch-скриптах съедает обратные слэши.** Дважды получил битые байты: +- `r'\1'` → **реальный байт `\x01`** в файле +- `r'\b'` → **реальный байт `\x08`** (backspace) + +Оба раза `compile()` проходил (валидный питон), но регекспы молча не работали. +**Правило: после записи патча проверять файл на байты `chr(1)` и `chr(8)`,** +а не только на синтаксис. Либо править через Obsidian-patch напрямую, а не генератором строк. + ## 🔴 2026-09-21: ДИСК 100% — инцидент и починка `sync-down` **Симптом:** диск `/dev/sda1` на Kraken — **0 байт свободно** (`df` 100%). Radarr API отдавал @@ -890,6 +1749,8 @@ Jellyfin library mapping: Movies → `movies`, Cartoons → `mixed` (movies + ep - ✅ **[2026-09-21] ФАЗА 2 ВЫПОЛНЕНА: фильтр ужасов + `⭐ > 🧟`** — коммит `2642882`. Точка — бывший `sync_down.py:54`. `⭐` обходит **все** фильтры (`continue` до проверок); ужас без `⭐` пропускается; рейтинг < 6.0 пропускается. Dry-run: **33 horror skipped**, 14 по рейтингу. ⚠️ `is_horror()` считает по жанрам TMDB (id 27), не по эмодзи — поэтому 33, а не 41. - ✅ **[2026-09-21] ФАЗА 3 ВЫПОЛНЕНА: надёжный `mark_queued`** — коммит `2642882`. Возвращает `bool`, поглощает эмодзи-префикс (`- [ ] ⬇️ 🧟♀️ Title`), обёртка `_mark()` в `sync_down.py` логирует провал `[!]`. Тесты 7/7 + проверка на копии боевого файла. **Корень потери 3 меток `⬇️`.** - ✅ **[2026-09-21] ФАЗА 4 ВЫПОЛНЕНА: `unmark_queued()` добавлена, `⬇️` снят** — у Шиндлера (396) и Неистового (107). Функции в коде **не было**. Тесты unmark 5/5 + round-trip. Бэкап `~/backup-movies-watchlist-unmark-20260921-152018.md`. +- ✅ **[2026-09-22] ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — ИСПРАВЛЕНО.** Симптом Alex: «Проект Конец света» в каталоге Lampa как непросмотренный, хотя в главном списке он `[x]`. Корень: `process_thumbs` обрабатывает только `👍`/`👎`/`[x]`; строка `- [ ] ⬇️ [Название](kp) (2025) IMDb 7.1` (без оценки) уходила в `keep` **навсегда**, а при наличии `[x]`-двойника давала дубль в каталоге. **Фикс:** сбор `watched_titles`/`watched_kp` по всем секциям кроме Новинок + удаление сирот из `keep`; нормализация **обязана срезать год и рейтинги**, иначе `проектконецсвета` ≠ `проектконецсвета2026imdb84kp84`. Флаг `_patch_dropped` добавлен в ранний выход (иначе запись файла пропускалась). `cli.py` — явные ветки вывода `👍/👎/[x]/drop`. Проверено: удаляется ровно 1 строка, 683 → 682; каталог 404 → 403. Бэкапы: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`, код — `parser.py.bak-orphan-*`, `.bak-guard-*`, `.bak-bs-*`, `cli.py.bak-cli-*`. Полный разбор — секция «ОСИРОТЕВШИЕ СТРОКИ» выше; связь с каталогом Lampa — [[personal/plans/lampac-watchlist-plugin]]. +- 🔴 **[2026-09-22] ПИТФОЛЛ правки `parser.py` скриптами: двойное экранирование съедает слэши.** `r'\1'` → реальный байт `\x01`, `r'\b'` → байт `\x08` (backspace). `compile()`/`ast.parse()` проходят — синтаксис валиден, а регекспы **молча не работают** (нормализация не срезала рейтинги, сироты не находились). **Правило: после записи патча проверять файл на `chr(1)` и `chr(8)`, а не только на синтаксис.** По возможности править через `patch`/Obsidian, а не генератором строк. - 🔴 **[2026-09-21] ФАЗА 5: ВЕРНУТЬ КРОН — ждёт.** `watchlist-nightly` (01:00) + `watchlist-discover` (вс 09:00) остановлены, бэкап `~/crontab-backup-20260921.txt`. Возвращать с **`--batch 2`** (не 5) и **только после проверки Фаз 1–2 в бою** (сначала dry-run). - 🔴 **[2026-09-21] ОТКРЫТЫЕ БАГИ `_clean()` — решение Alex НЕ принято.** Баг **A** (`parser.py:47`): `(?:/\d{4})+` требует 2+ варианта года → одиночный `(2022)` не срезается, уходит в `resolve()`. Баг **B** (`parser.py:19-38`): `_EMOJI_PREFIX` заканчивается на `\s*` → берёт только первый эмодзи, `🧟♀️` остаётся в `title`. Оба **существовали до правок** (проверено на старом коде), не регрессия. Лечение B — форма `^(?:[<символы>]+\s*)+`. - 🔴 **[2026-09-21] `_mark_watched` в `sync_up.py` — исправлено локально, НЕ залито/НЕ закоммичено.** Тот же класс бага: строка `- [ ] ⬇️ 🧟♀️ Title` не перешла бы в `[x]`. ⚠️ Правка Фазы 3 **создаёт** именно такой формат → чинить синхронно с возвратом крона, иначе `sync-up` перестанет видеть новые закачки. diff --git a/personal/plans/lampac-watchlist-plugin.md b/personal/plans/lampac-watchlist-plugin.md index e03778d2..6bbf9d49 100644 --- a/personal/plans/lampac-watchlist-plugin.md +++ b/personal/plans/lampac-watchlist-plugin.md @@ -296,6 +296,17 @@ Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js 6. **Кириллица в inline-jq внутри `ssh '...'`** ломает команду (tirith + quoting). Выносить в файл или убирать строки с кириллицей. +7. **Осиротевшие строки в `## Новинки` давали дубли в каталоге.** + Строки без `👍`/`👎` (например `- [ ] ⬇️ Название`) `process-thumbs` не трогал — + они висели в Новинках навсегда. Если фильм уже `[x]` в главном списке — получался дубль: + непросмотренный «Конец света» при просмотренном оригинале. + **Фикс в `watchlist-sync/parser.py`** (удаление сирот + флаг `_patch_dropped`) и `cli.py`. + Разбор: [[family/projects/watchlist-automation]], секция «ОСИРОТЕВШИЕ СТРОКИ». + +8. **Двойное экранирование при правке `parser.py` скриптами.** + `r'\1'` превращался в байт `\x01`, `r'\b'` — в `\x08`. Синтаксис валиден, регекспы молчат. + **Проверять файл на `chr(1)` и `chr(8)` после патча.** + ### Управление ```bash @@ -324,7 +335,6 @@ ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \ ``` ## Правила отбора и сортировки (утверждено Alex 2026-09-22) - ### Фильтрация | Правило | Действие |