--- created: '2026-05-20' updated: '2026-09-27' status: active tags: - kraken - watchlist - automation - movies --- # Watchlist Automation — Полный Flow Полная цепочка: как `family/documents/movies-watchlist.md` превращается в скачанный контент. ## ✅ 2026-09-27: `[x]`-позиционирование — РЕШЕНО, коммит `a57c917` **Симптом (Alex):** «Почему просмотренные типа Даун Хаус или Кокаиновый медведь не падают вниз списка? Да и новинки как будто давно не обновлялись». **Спецификация** ([[family/projects/watchlist-sync]] §1.4): строка встаёт **под последней `[ ]` своей секции**, т.е. в **НАЧАЛО блока просмотренных** этой секции. Для ВСЕХ секций со списком (Фильмы / Мультфильмы / Аниме). НЕ «в начало файла», НЕ «в конец файла», НЕ в отдельную секцию. Смысл слова Alex «вниз» = уход из зоны `[ ]` вниз, в блок `[x]`. ### ✅ РЕШЕНИЕ: `reflow_watched()` в `sync_up.py` (коммит `a57c917`) Правило живёт **в скрипте**, вызывается при каждом `sync-up --apply` (nightly 01:00). Обрабатывает и новые отметки, и уже накопленные. Идемпотентна: повторный прогон = 0 перемещений. ```python # sync_up.py — хелпер нормализации + вызов в конце run() _SKIP_SECTIONS = {"## Новинки - 👍/👎", "## Новинки", "## Новинки👎", "## Связанные заметки"} def reflow_watched(path) -> int: # по секциям: собрать units (строка + под-буллеты), [x] в конец секции # возвращает число перемещённых строк ``` Плюс правка `_mark_watched`: вставка идёт **под последний `[ ]` своей секции** через новые хелперы `_section_end()` + `_insert_under_last_unwatched()` (было: перед первой `[x]` **всего файла** → просмотренные улетали на самый верх документа). **Бэкапы кода:** `sync_up.py.bak-xdown`, `sync_up.py.bak-reflowfn`. ### ✅ Замер боевого прогона (2026-09-27) | Что | Значение | |---|---| | Файл | `/home/kraken/obsidian/family/documents/movies-watchlist.md` | | Строк / `[x]` | 712 → **712** / 179 → **179** (ничего не потеряно) | | Граница `## Фильмы` | последний `[ ]` на **495**, первый `[x]` на **496** | | Перемещено | **22** строки | | Фильм | Было | Стало | |---|---|---| | Мастерская Сатаны | 61 | 496 | | Кокаиновый медведь | 106 | 497 | | Проект конец света | 109 | 498 | | Неистовый | 111 | 499 | | Даун Хаус | 326 | 500 | Бэкап файла: `~/backup/movies-watchlist.md.before-reflow-20260927-221616`. Проверка идемпотентности: второй прогон → `moved: 0`. ### 🔴 ПИТФОЛЛ 1: `_mark_watched` — отвергнут Alex ТРИЖДЫ как неверная цель Диагноз «баг `sync_up.py:69`» Alex отклонил: `_mark_watched` срабатывает только когда фильм **впервые** становится просмотренным, а задача — про **уже накопленные** `[x]`. Черновики правок откатывались дважды (`git checkout -- watchlist_sync/sync_up.py`). Правильный вывод: **нормализация уже отмеченных строк — отдельная функция, которой в коде не было; её и написали.** Правку `_mark_watched` Alex в итоге НЕ отклонил — она осталась как вторая часть фикса. ### 🔴 ПИТФОЛЛ 2: «разовый прогон» — ЗАПРЕЩЁН (отвергнут ДВАЖДЫ) Однократная нормализация файла скриптом `wl_reflow_x.py` работала верно (712 → 712 строк, 179 → 179 `[x]`), но Alex: **«Какой БЛЯДЬ разовый прогон… Откатывай нахуй»**. Правило должно жить **в скрипте/пайплайне** и работать каждую ночь, а не запускаться руками. **Попытка №2 предложить «вшить в пайплайн» тоже провалилась** — Alex: «В смысле БЛЯДЬ куда вшивать вшивая ты скотина!!!» → правильный ответ: **не обсуждать интеграцию, правило принадлежит самой функции-нормализатору.** Вопрос «sync-up или process-thumbs?» задавать НЕЛЬЗЯ. Финал: Alex → «В скрипт блядь». ### ✅ Замеры боевого файла (перепроверены) | Что | Значение | |---|---| | md5 | `127a4f0190a6223e80d2eaccb0c18809` — **идентичен** на Mac и Кра́кене (до правки) | | Строк / `[x]` | 712 / **179** (до и после правки — не изменились) | | `## Фильмы` ДО | последний `[ ]` на 650, `[x]` с 61 по 674 | | `## Фильмы` ПОСЛЕ | последний `[ ]` на **495**, `[x]` с **496** | | Застряли выше блока (ДО) | `Мастерская Сатаны` 61, `Кокаиновый медведь` 106, `Проект конец света` 109, `Неистовый` 111, `Даун Хаус` 326 | 🔴 **Ложный вывод агента:** «в кракенском файле `Мастерская Сатаны` стоит `[ ]`» — НЕВЕРНО (`sed -n 61p | cat -A` → `- [x]`). Файлы идентичны по md5. **Перед утверждением о расхождении машин — сверять md5 обеих сторон.** ### Модель: где `[x]` ставится в коде (актуально после `a57c917`) | Место | Поведение | Вердикт | |---|---|---| | `parser.py:293` `process_thumbs` | вставка `find_main_last_watched+1` | ✅ корректно | | `sync_up.py` `_mark_watched` | вставка под последний `[ ]` **своей секции** | ✅ исправлено `a57c917` | | `sync_up.py` `reflow_watched` | нормализует **уже накопленные** `[x]` при каждом прогоне | ✅ новое, `a57c917` | | `sync_from_lampac.py:259` | вызывает `sync_up._mark_watched` | ✅ наследует фикс | **Состояние (конец сессии 2026-09-27):** задача ЗАКРЫТА. - `cf73666` — коммит ДО правки (зафиксировано чужое изменение `sync_down.py`). - `a57c917` — правка: `reflow_watched()` + фикс `_mark_watched`. - Боевой файл приведён к правилу, проверено 712/179 и идемпотентность. - Порядок работ: **коммит ДО → правка → деплой → прогон.** Бэкап в план не входил. 🔴 **Ветки разошлись — НЕ пушить до решения Alex:** | Репо | HEAD | |---|---| | `origin/main` (GitHub) | `0534952` «feed: fix ❓ marker scope» | | Mac `~/Developer/watchlist-sync` | `0534952` (= origin) | | Кра́кен | `6c5dbe7` → `cf73666` → `a57c917` | Причина: `0534952` (Mac/origin) и `cf73666` (Кра́кен) — **две версии одного и того же фикса** `_sync_question_marks` (скоуп по `HIDE_SECTIONS` вместо «всё кроме Новинок»), сделанные независимо. `cf73666` был незакоммичен на Кра́кене и зафиксирован агентом. Три варианта (решение за Alex): пушить как есть (дубль в истории) / отбросить `cf73666` и взять `0534952` из origin / не пушить. ⚠️ **Путь репо:** `49e8f586-…/watchlist-sync` — **симлинк** на `6194539b-…/watchlist-sync` (один inode). `.pth` editable-установки указывает на `6194539b` — это не два репозитория. **«Новинки не обновлялись» — две разные вещи:** 1. Каталог Lampa обновляется каждую ночь: `watchlist.json` 27.09 01:07, `count: 403` (films 357 / new 32 / cartoons 7 / anime 7). 2. `## Новинки` в `.md` — последний `discover` 21.09. 27.09 (вс) скрипт залогировал только sync-vault pull/push, без шага `discover`. **Причина не выяснена — открытый вопрос.** Плюс `sync_down.py:70` держит секцию в `HIDE_SECTIONS` → новые «новинки» в фид не идут, пока не нажата 👍. **Питфоллы jq по фиду:** ```bash F=.../lampac/wwwroot/watchlist.json jq -r '.channels | to_entries[] | "\(.key) (\(.value.title)): \(.value.items | length)"' $F jq -r '.count' $F # 403 ``` 🔴 Канал — объект `{title, items}`, НЕ массив → `jq -c ".channels.films | .[0]"` даёт `Cannot index object with number`. 🔴 `## Новинки` в HIDE_SECTIONS → `grep` по фиду НЕ найдёт позиции из Новинок. Фид содержит только `Status.UNWATCHED` (`sync_down.py:326`) — ни одного `[x]`. ## ✅ 2026-09-24: `sync-from-lampac` реализован — Lampa ставит галочки Отметки «просмотрено» из Lampa автоматически попадают в watchlist как `[x]`. Коммит `ef44f3e` (код), план — [[personal/plans/lampac-watched-to-watchlist]] §10. **Ночной пайплайн теперь:** ``` sync-vault (pull) → process-thumbs → resolve --recheck → [sync-down ПРОПУЩЕН] → sync-up (Jellyfin, non-fatal) → sync-from-lampac --apply (NEW) → sync-vault (push) ``` **Как работает матч** (`watchlist_sync/sync_from_lampac.py`): ``` timecodes.identity ('movie-674' / '674_movie', строгие формы) → tmdb_id → 1) .resolve_cache.json (candidate.tmdb_id → ключ = заголовок .md) — ТОЧНО → 2) фолбэк: watchlist.json (href → name TMDB-ru / original + год ±1) → _mark_watched ([x]) — ⚠️ ИСПРАВЛЕНО 2026-09-27: ФАКТИЧЕСКИ вставляется НЕ в блок просмотренных, а «выше первой [x] в файле» — баг, см. ниже «2026-09-27» ``` **Критерий просмотренного:** `deleted=0` И `percent >= 90` (env `LAMPAC_PERCENT_THRESHOLD`, CLI `--percent`). `watched_at` — НЕ критерий (бывает >0 при 18%). Сериалы `tv-*` — скип, этап 2. **Ключевые факты:** - ✅ БД `TimeCode.sql` — **на ХОСТЕ** (bind-mount `./database:/lampac/database`, исправлено 2026-09-24 13:38): `/srv/dev-disk-by-uuid-6194539b-…/lampac/database/TimeCode.sql`, модуль читает напрямую, **`docker cp` больше не нужен**. ⚠️ Раньше было «только внутри контейнера, НЕ на volume» — УСТАРЕЛО. WAL: свежие данные в `-wal`, голый `cp` одного `.sql` = пустая оболочка; читать каталог целиком или `sqlite3 X ".backup Y"`. `sqlite3` в контейнере нет. - `Sync.sql` (bookmarks) и `Sisi.sql` — ПУСТЫЕ: закладки Lampa не синхронизируются на Lampac, TimeCode — единственный канал просмотров. - `timecodes`: UNIQUE(user, identity) — одна строка на юзера+фильм, percent обновляется на месте. - Ручной прогон: `uv run watchlist-sync sync-from-lampac` (dry-run по умолчанию, `--apply` для записи). **.env (Kraken, `$WS/.env`, бэкап `.env.bak-lampac-20260924`):** ``` LAMPAC_TIMECODE_DB=lampac:/lampac/database/TimeCode.sql LAMPAC_WATCHLIST_JSON=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/wwwroot/watchlist.json LAMPAC_PERCENT_THRESHOLD=90 ``` **nightly:** md5 новой `edf0e34530692dbb7b6fb9644f1b45a7` (было `4401882e...`), бэкап `~/scripts/watchlist-nightly.sh.bak-lampac-20260924`. Полный ручной прогон 01:05–01:07 ✓, новый шаг ~2 сек, non-fatal. **Побочный фикс:** `_normalize` в `sync_up.py` не делал `.strip()` после удаления небуквенных — заголовки с хвостовым ⭐ молча проваливали `_mark_watched` (в т.ч. в Jellyfin-канале). Фикс в `ef44f3e`. **Тест:** синтетическая БД (8 сценариев: cache/json матч, 18%, tv, deleted, уже-[x], левый id, dry-run) — 8/8 ✓; `--apply` на копии .md — diff ровно 2 ожидаемые строки. Бэкап до работы: `~/backup-watchlist-before-lampac-sync-20260924-005630/`. **Осталось:** калибровка порога живым досмотром (досмотреть фильм из списка в Lampa, вычитать percent, при ≠90 поправить env); сериалы этап 2; bind-mount для `/lampac/database` (риск потери истории при пересоздании контейнера — отдельный апрув). Проверка в UI: после первого реального досмотра фильм пропадает из «Мой список» в Lampa после ночного прогона (каталог 02:30 выкидывает `[x]`). ## 🔴 2026-09-27: `[x]` НЕ падает вниз списка — баг `_mark_watched` (диагноз) **Симптом Alex:** «Почему просмотренные в watchlist типа Даун Хаус или Кокаиновый медведь не падают вниз списка? Да и новинки как будто давно не обновлялись» **Корень** — `watchlist_sync/sync_up.py:69`, `_mark_watched`: ```python insert_at = next((i for i, l in enumerate(lines) if re.match(r'\s*-\s*\[x\]', l)), None) lines.insert(insert_at, raw) ``` `next(...)` = **первая** `[x]`-строка файла. Докстринг обещает `top of the [x] section`, фактически получается `top of file`. Расхождение кода и комментария = баг. **Факт из `.md` 27.09:** `- [x] Даун Хаус` — строка **326**, окружена `[ ]`-соседями. Блок просмотренных реально: **421 → 674**. `Кокаиновый медведь` — строка 106, `Проект конец света` — 109. Между ними и блоком ~130 строк со `[ ]`. **Аудит:** 178 `[x]`-строк в главных секциях, из них **4** выше блока 421–674 — все четыре пришли от `sync-from-lampac` после 25.09. Остальные 174 уже стояли в блоке, и вставка «выше первой `[x]`» для них работала правильно. **Готовый инструмент уже есть:** `parser.py:401 find_main_last_watched(ls)` — её использует только `process_thumbs` (`insert_at = last_watched + 1`). `_mark_watched` её не зовёт. Фикс — ~5 строк, **НЕ применён, ждёт команды Alex**. **Каталог Lampa при этом НЕ затронут** — проверено: `watchlist.json` 27.09 01:07, `count: 403` (films 357 / new 32 / cartoons 7 / anime 7), ни одного `[x]` в фиде. Ночной пайплайн отработал полностью, бэкап `TimeCode.sql` за 27.09 в 01:09 создан. **Второй симптом — «новинки не обновлялись»:** каталог обновляется каждую ночь, а секция `## Новинки` в `.md` — нет: последний `discover` был **21.09**. Крон стоит (`0 9 * * 0`), но в логе `watchlist-discover.sh` за 27.09 (воскресенье) шага `discover` нет — только `sync-vault` pull и push, причём push на 5 минут раньше pull. **`jq`-питфолл фида:** `.channels.films | length` → **2** (ключи `items`/`title`), а не число позиций. Правильно: `.channels.films.items | length` → 357. Полный разбор, аудит и план фикса — [[personal/plans/lampac-watched-to-watchlist]] §11. ## 🔴 2026-09-23: канал «Lampa → `[x]`» найден; фиксы сирот закоммичены ### Коммит `ffe7974` — закрыт должок по грязному дереву В рабочем дереве `watchlist-sync` висели **незакоммиченные** правки, которые уже работали боевым прогоном (ночной 02:30 использовал именно их): | Файл | Δ | Что | |---|---|---| | `watchlist_sync/parser.py` | +65 | фикс «ОСИРОТЕВШИЕ СТРОКИ» в `process_thumbs()`: `watched_titles`/`watched_kp` по всем секциям кроме Новинок, `_norm_title()`, вырезание сирот из `keep`, флаг `_patch_dropped` в раннем выходе | | `watchlist_sync/cli.py` | +7 | явные ветки вывода `👍 / 👎 / [x] / [drop]` (было обобщённое `else`) | Закоммичены **отдельным** коммитом, не смешаны с новым кодом. Предыдущий HEAD был `a131208` (2026-09-21). `sync_up._mark_watched` **уже содержит** фикс эмодзи-префикса — `_PREFIX` поглощает эмодзи с обеих сторон `⬇️`. Работает, правок не требует, переиспользуется. ### Найден реальный канал просмотров — модуль Lampac `TimeCode` `sync_up` читает **Jellyfin**, но Jellyfin больше не источник: `BaseItems` — 53, `movie` — **0**, `UserData.IsPlayed=true` — **0**. Смотреть стали через Lampa+TorrServer. **Фактический источник просмотров — Lampa**, и она уже пишет их на сервер в SQLite-БД модуля `TimeCode`: | | | |---|---| | БД | `$H/lampac/database/TimeCode.sql` (таблица `timecodes`) | | Модуль | `/lampac/module/Sync/TimeCode/` (`Controller.cs`, `plugin.js`, `README.md`) | | Ключ | `identity` = TMDB-ID (`movie-1758655` / `1758655_movie`) | | Прогресс | `percent` (флага «просмотрено» нет — политика клиента, см. README) | 🔴 **`watched_at` — НЕ признак досмотра.** Живая запись: `watched_at = 1790053739498` при **`percent = 18`**. Значит любое обновление таймкода пишет `watched_at`. Порог ставить **только по `percent`**. План реализации синка `timecodes` → `[x]`: [[personal/plans/lampac-watched-to-watchlist]]. ### ✅ `.14` = провод, `.15` = WiFi. Оба — один Кра́кен (Alex, 2026-09-23) 🔴 **Ранее в этом доке было «`ssh kraken` резолвится в мёртвый `.15`» — ЭТО НЕВЕРНО.** `.15` не мёртвый: это **WiFi** (`wlan0`), `.14` — **провод** (`eth0`), ходим по `.14`. ``` eth0 UP 192.168.1.14/24 metric 100 ← основной, default route wlan0 DOWN ← отключён (см. ниже) ``` **WiFi `wlan0` ОТКЛЮЧЁН (2026-09-23).** `.15` больше не выдаётся, остался только провод `.14`. При попытке гасить интерфейс: `ip link set wlan0 down` **не держится** — `systemd-networkd` поднимает обратно (`Activation Policy: up` от wpa_supplicant). Рабочий путь — правка `/etc/netplan/30-wlan0.yaml` + `netplan apply`. ⚠️ `rfkill` на Кра́кене **НЕТ** (`command not found`). **Настоящие причины обрывов ssh** (обе воспроизведены 2026-09-23): 1. **Перегрузка sshd** — падало при `load average: 4.69` (после ребута поднимались контейнеры). Симптом: `Connection closed by UNKNOWN port 65535` / `Connection timed out during banner exchange`. Лечится повтором и короткими командами, не сменой адреса. 2. 🔴 **`sudo` через SSH без `< /dev/null` рвёт соединение.** Проверено многократно: ```bash ssh kraken 'sudo docker restart x && sudo docker ps' # ❌ рвётся ssh kraken 'sudo docker restart x < /dev/null && … < /dev/null' # ✅ ``` Надёжнее — весь скрипт файлом: `scp` → `sudo bash /tmp/x.sh < /dev/null`. 3. **Тяжёлый `jq` в одной строке с `ssh`** (json 180 KB) — обрыв. Разбивать, тяжёлое — файлом. **DNS-имя `kraken` — ИСПРАВЛЕНО И РАБОТАЕТ (2026-09-23).** Было: `dhcp_staticlist` нёс имя `kraken` у **обоих** интерфейсов, плюс `dnsmasq.conf.add` имел `address=/kraken/192.168.1.15` (жёстко на WiFi). Стало: `address=/kraken/192.168.1.14`, имя у wlan0-лиза снято. Ребут роутера **НЕ потребовался**. Проверено: `dig +short kraken` → `.14`, ping идёт, `ssh kraken@kraken` заходит. 🔴 **Настоящая причина «залипшего» `.15` (стоила часов — не повторять):** | Действие | Почему НЕ помогало | |---|---| | Удаление лизы из `/var/lib/misc/dnsmasq.leases` | **Не тот файл** — рабочий в `/tmp/var/lib/misc/dnsmasq.leases` | | `service restart_dnsmasq` | **Не убивает процессы** — старый ответ живёт в памяти | | `killall -SIGHUP dnsmasq` | Не сбрасывает DNS-кэш ответов | | `dhcp-ignore-names=lan` | Неверный синтаксис (нужен `tag:`); строку убрать | | `dnsmasq.postconf` скрипт | Не нужен — ASUSWRT читает `conf.add` нативно; даёт дубль блока в конфиге | **Что сработало:** ```bash # рабочий лиз-файл — /tmp, чистить его ssh admin@192.168.1.1 'grep -v "192.168.1.15" /tmp/var/lib/misc/dnsmasq.leases > /tmp/l.new \ && cat /tmp/l.new > /tmp/var/lib/misc/dnsmasq.leases' # ЖЁСТКО убить процессы (restart_dnsmasq их не убивает!) ssh admin@192.168.1.1 'killall -9 dnsmasq; sleep 2; service restart_dnsmasq; sleep 6' # проверить sudo dscacheutil -flushcache; dig +short kraken @192.168.1.1 # → 192.168.1.14 ``` ⚠️ `reboot` роутера **из агента невозможен** — в жёстком блоклисте Hermes (`BLOCKED (hardline)`). `pgrep`/`rfkill` на роутере и Кра́кене отсутствуют. Определить рабочий лиз-файл: `for p in ; do ls -l /proc/$p/fd | grep leases; done`. Полный разбор: скилл `home-network-config` § «РЕШЕНО БЕЗ РЕБУТА». ### Состояние стека после ребута (замер 2026-09-23) 14/14 контейнеров поднялись сами. Radarr/Sonarr queue `0`, Transmission 9 торрентов всё `Done/Idle` (Sum down `0.0`). Диск **31%** (539G/1.8T). Lampac `:9118` → 200, Jellyfin `:8096` → 302. Ночные скрипты отработали: cleanup 02:00, nightly 01:00, catalog 02:30–02:32 (каналов 4, позиций 392; `.md` 708 строк / 615 позиций). ✅ **DNS `kraken` → `.14` — РАБОТАЕТ** (проверено 2026-09-23, без ребута роутера). `wlan0` DOWN, `.15` не выдаётся. Разбор граблей — в разделе про DNS ниже. #### ✅ Проверка API стека — рабочая последовательность (2026-09-23) Ключи читать **с диска**, не через `docker exec`: ```bash D="/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker" RK=$(grep -o '[^<]*' $D/radarr/config/config.xml | cut -d'>' -f2) curl -s --max-time 10 "http://localhost:7878/radarr/api/v3/queue?pageSize=50" \ -H "X-Api-Key: $RK" | jq -r '.totalRecords' # → 0 # sonarr: порт 8989, путь .../sonarr/config/config.xml ``` ⚠️ `docker exec` без `< /dev/null` **рвёт SSH-сессию**. Скрипт передавать целиком: `ssh kraken 'bash -s' < /tmp/script.sh` — устойчиво, в отличие от длинных inline-команд. Результат 2026-09-23: Radarr queue `0`, Sonarr queue `0`. ## 🔴 2026-09-22: АВТОСКАЧКА НА HDD ВЫКЛЮЧЕНА (по требованию Alex) ### ✅ 2026-09-24: защита СНЯТА (по требованию Alex) — актуальное состояние Логика: из watchlist в arr автоматом ничего не добавляется, защита мешала только ручным добавлениям («для вручную добавленных закачка должна стартовать»). | Что | Было (защита 2026-09-22) | Стало (2026-09-24) | |---|---|---| | `arr.py`: `monitored` + `searchForMovie` / `searchForMissingEpisodes` | `False` | **`True`** — коммит `ef7e7af` (push): добавление сразу ищет и качает. 🔴 **Вечером того же дня `arr.py` УДАЛЁН** (`a524f5d`) вместе с переходом `sync-down` на фид Lampa — см. «Консолидация» ниже | | Radarr `rssSyncInterval` | `0` | **15 мин** (`PUT /api/v3/config/indexer`, HTTP 202) | | Sonarr `rssSyncInterval` | `0` | **15 мин** (там же) | | `sync-down` в кроне | вырезан | **остался вырезан** — авто-пути из watchlist нет, добавление только руками | | `monitored` существующих (34 фильма / 13 сериалов, все без файлов) | снят | **контент удалён целиком** — см. ниже «контент удалён» | Проверки после: очереди Radarr/Sonarr `0`, `monitored=true: 0 из 34`, auth не тронут (`forms` / `/radarr` / 7878), RSS-задача `next − last = 900s`. 🔴 **Питфолл Radarr 6.4 / Sonarr 4.0.20:** `rssSyncInterval` живёт НЕ в `/api/v3/config/host` (поля там физически нет), а в **`/api/v3/config/indexer`** — PUT полным объектом с `id` (частичный молча возвращает старое). Через host-config тупик: валидатор требует непустой `allowedHosts` при `authenticationRequired != Enabled` и не принимает звёздочку. **✅ Консолидация РЕАЛИЗОВАНА 2026-09-24:** генератор каталога слит в `sync-down`. Было две независимых цепочки — `lampac/scripts/gen_watchlist.py` (878 строк, свой матчер TMDB, не знал про `resolve` и его маркеры) и `sync_down.py` (arr-путь). Теперь **один модуль**: `sync-down` резолвит через `resolver.resolve()` и пишет фид Lampa (`wwwroot/watchlist.json`, `watchlist-.json`, `cards/`, `cards.json`, `missing.json`). `arr.py` **удалён** (`git rm`, коммит `a524f5d`), ⬇️ больше не ставится нигде. Детали, три открытия при переносе (фильтры рейтинга/ужасов в фиде НЕ нужны; ❓ не исключает позицию из фида; спорные позиции берутся из `all_candidates[0]` + ❓) и открытые пункты — [[personal/plans/lampac-watchlist-plugin]] § «ПЕРЕПИСЫВАНИЕ». 🔴 **Блокер выката (2026-09-24):** резолвер проекта матчит хуже старого генератора — сверка по `tmdb_id` дала **86 позиций с другим фильмом**. Причина: в `gen_watchlist.py` есть ступень «КП-по-номеру из ссылки» (`kinopoisk.ru/film/N/` → КП → TMDB), в `resolver.py` её нет; до удаления arr-контента это маскировал Radarr-фастпатч. На живой фид НЕ выкатывалось. Поэтому `lampac/scripts/gen_watchlist.py`, `mark_missing.py` и `watchlist-catalog.sh` (крон 02:30) **пока НЕ удалены** — оттуда берётся код `kp_search_by_id` / `_docs_to_tmdb`. Метрики dry-run: 268 (с фильтрами) → 332 (без фильтров) → **397** (с `all_candidates[0]`). ### ✅ 2026-09-24 (вечер): контент удалён — Radarr/Sonarr пустые Alex: «если они ни в чем не участвуют, удали все фильмы/сериалы». Проверка перед удалением: каталог Lampa (`gen_watchlist.py`) использовал Radarr только как фаст-патч постеров/tmdbId — **9 карточек из 392** (2%), остальные 362 через TMDB API (основной путь, кэш 434 записи), сериалов в каталоге нет вообще, import-lists пустые, файлов у контента ноль. **Удалено:** 34/34 фильма (Radarr) + 13/13 сериалов (Sonarr), `deleteFiles=false`, `addImportExclusion=false`, ноль ошибок. Бэкапы: `radarr.db.bak-20260924-163600`, `sonarr.db.bak-20260924-163600` (в config-каталогах; онлайн `.backup`, рестарта не было). Очереди после: 0/0. **Регрессионный тест** (прогон генератора с пустым Radarr в `/tmp`, живой каталог не трогал): | Метрика | утро (с Radarr) | после удаления | |---|---|---| | карточек | 392 | **392** | | без TMDB (→❓) | 30 | **30** | | постеров | 388 | **390** | | источники | Radarr 9, TMDB 362 | Radarr 0, TMDB 371 | Ноль потерь. **3 карточки сменили матч** (в `.md` нет года, Radarr и TMDB матчили вслепую): - `Unbroken` 2003 → **Несломленный 2014** (вероятно правильнее) - `Shogun` 1968 → **Убийца сёгуна 1980** - `Олдбой` 2003 → **Олдбой 2013** (⚠️ ремейк Спайка Ли; если имелся в виду оригинал Пака Чхан-ука — добавить год в строку 134 `.md`: `Олдбой 2003`) Фаст-патч Radarr в генераторе оставлен — при пустом Radarr молча не срабатывает, оживёт сам при ручных добавлениях. ### ✅ 2026-09-24 (вечер): отдельный генератор Lampa ВЫПИЛЕН — фид пишет sync-down **Требование Alex:** «не отдельный скрипт на лампу, а один sync скрипт и писал сразу lampa feed». Раньше связка была двумя механизмами: `sync-down` (отдельный шаг) + `lampac/scripts/gen_watchlist.py` (878 строк) по своему крону в 02:30. **Стало:** `watchlist_sync/sync_down.py` собирает фид сам, в ночном пайплайне на своём прежнем месте (01:00), генератор и его обвязка удалены: | Было | Стало | |---|---| | `sync-down` → Radarr/Sonarr (`arr.py`) | `sync-down` → `wwwroot/watchlist.json` + `cards/` + `missing.json` | | `lampac/scripts/gen_watchlist.py` (878) + `mark_missing.py` (179) | `.bak-removed-20260924-181942` | | `~/scripts/watchlist-catalog.sh` + крон `30 2 * * *` | убран (`.bak-removed-…`), крон почищен | | `arr.py` (Radarr/Sonarr API) | удалён из репы (в истории git) | | маркер `⬇️` | больше не ставится — закачки нет, маркер ничего не значит | Коммиты: `a524f5d` (перенос), `471e9df` (ступень КП + тайбрейк), `0534952` (скоуп ❓). **Резолв фида — 2 ступени** (`sync_down.run`): 1. **КП-по-номеру из ссылки `.md`** (`resolver.resolve_by_kp_id`) — машинный ключ, не зависит от переименований. Без этой ступени на названиях расходилось ~86 позиций. 2. **Матч по названию** (`resolver.resolve`). Неоднозначные → берётся кандидат с лучшим `score`, ничьи разбивает рейтинг (Alex 2026-09-24) + ❓ в `.md`. 🔴 Только рейтинг использовать НЕЛЬЗЯ: выбрал бы «Семь Самураев» (★8.5) вместо «Семь» (1995, ★8.4) и «Злая наука» вместо нужного фильма. **Питфоллы, найденные по ходу:** - 🔴 Фильтры рейтинга/ужасов в фиде — НЕДОПУСТИМЫ: это правила закачки Radarr, а не каталога. С ними терялось ~120 позиций (37 ужасов). Каталог отдаёт всё. - 🔴 `resolve_by_kp_id` не заменяет поиск: у 66 строк с ❓ ссылки на Кинопоиск в `.md` НЕТ, они идут в слепой матч по названию. Куда показывать — уточнять год. - 🔴 `resolve` с годом-хинтом не фильтровал КП-кандидатов: «Злая 2024» → `Cattiva` (1991) со score 1.0. Исправлено — год отсекает КП-документы. - 🔴 `_sync_question_marks` считал скрытым всё, кроме секции «Новинки» → ❓ не попадал в Фильмы/Мультфильмы/Аниме. Скрывать можно только `HIDE_SECTIONS`. - `WatchItem` нельзя класть в выходной JSON (не сериализуется) — только словари. **Проверено на живом:** фид 390 позиций (4 канала), lampac HTTP 200, фид 200, карточка 200, ночной прогон целиком зелёный, vault запушен. Бэкап фида до правки: `/tmp/feed-before-*.json`; ночного скрипта — `watchlist-nightly.sh.bak-feed-20260924-181926`. 🔴 **Рестарт lampac после обновления фида НЕ НУЖЕН** (уточнено 2026-09-24, вопрос Alex). `wwwroot/` — bind-mount, скрипт пишет файл **с хоста**, внутри контейнера он виден сразу (проверено: один и тот же inode `119669729` и размер на хосте и в `lampac`). Алекс: «обновление фида же не потребует передёрга?» — верно, не требует. `docker restart lampac` был унаследован от старого `catalog.sh`, который так сбрасывал `hybridCache` каталога; цена — приложение падает на ~минуту (компиляция модулей, HTTP `000`). Из ночного скрипта рестарт убран. Если каталог в Lampa залип (кэш-ключ `catalog:{plugin}:{search}:{sort}:{cat}:{page}` живёт в памяти процесса) — обновить страницу в Lampa или переключить категорию, а не ронять контейнер. Дальше — история и cookbook той защиты. **Контекст.** Задача — Lampa на ТВ показывает каталог из `movies-watchlist.md` и играет через TorrServer. Alex: «мы перестаем качать торренты на диск!» → автоскачка Radarr/Sonarr выключена фактически. Модуль Catalog/Lampa — в [[personal/plans/lampac-watchlist-plugin]]. ### Выполнено (замер до → после) | Сервис | Очередь | `monitored` | `rssSyncInterval` | |---|---|---|---| | **Radarr** (:7878) | 6 → **0** | 45 → **0** | 30 → **0** | | **Sonarr** (:8989) | 19 → **0** | 13 → **0** | 15 → **0** | Бэкапы: `radarr.db` / `radarr.db.bak-20260922-123404`, `sonarr.db` / `.bak-20260922-123404` (+ оба `config.xml`) в `/srv/...6194539b.../docker/{radarr,sonarr}/config/`. ~~**Метаданные НЕ удалены** — это важно: 50 фильмов остались в Radarr с `tmdbId` (все 50), постерами (46) и рейтингом. Они нужны каталогу Lampa.~~ 24 файла на диске (Jellyfin) не тронуты. 🔴 Устарело 2026-09-24: контент удалён полностью; каталог Lampa перешёл на TMDB-фолбэк без потерь (392 карточки, см. выше «контент удалён»). Опасение «нужны каталогу» не подтвердилось — Radarr обслуживал 2% карточек как кэш. **Проверка пассивности:** Transmission за 45 с — NET `290GB/26.7GB`, BLOCK `214GB/393kB` **не сдвинулись**. Логи Radarr (`grep -iE "search|download|grab"`) за 2 мин — пусто. ### ✅ 2026-09-22 (вечер): `sync-down` вырезан из крона — дыра закрыта СНАРУЖИ Второй слой защиты после ручного снятия `monitored`. Даже если `arr.py` не тронут, **автоматического пути в закачку больше нет вообще**: `watchlist-nightly.sh` вызывал `sync-down --apply --batch 2`. Вызов **убран**. Крон теперь: `sync-vault → process-thumbs → resolve --recheck → sync-up → sync-vault`. Бэкап: `/home/kraken/scripts/watchlist-nightly.sh.bak-20260922-182655`. Сумма новой: `4401882e3b1307a73b3330a70b59ecfe`. 🔴 **Порядок защиты** (действовал 2026-09-22 → 2026-09-24; частично снята — см. секцию выше): 1. Ручное снятие `monitored` + `rssSyncInterval: 0` — **отменено 2026-09-24** (rss=15; `monitored` существующих оставлен снятым). 2. **`sync-down` вне крона** — остаётся и после 2026-09-24 (авто-добавлений из watchlist нет). `sync_down.py` как код оставлен — ручной запуск возможен. Проверка отсутствия автозакачки: ```bash ssh kraken "grep -ln 'radarr_add\|sonarr_add\|radarr_search\|sync-down --apply' \ /home/kraken/scripts/*.sh" ``` Должны быть только комментарии. Детали — [[family/projects/watchlist-sync]]. ### ✅ 2026-09-23: пост-ребут проверка стека — все чисто После перезагрузки Кра́кена (uptime 23 мин на момент проверки) — **все 14 контейнеров поднялись сами**, ручного вмешательства не потребовалось. | Проверка | Значение | |---|---| | Radarr queue | `totalRecords: 0` | | Sonarr queue | `totalRecords: 0` | | Transmission | 9 торрентов, все `100% Done / Idle`, Sum down `0.0` | | Lampac `:9118` | HTTP 200 | | Jellyfin `:8096` | HTTP 302 | | Диск `/dev/sda1` | 539G / 1.8T = **31%** | **Ночные скрипты отработали** (все `2026-09-23`): `cleanup` 02:00 (27.9% → ниже порога, no-op), `nightly` 01:00, `catalog` 02:30→02:32 (каналов 4, позиций 392), простановка ❓ 02:32 (708 строк, 615 позиций, `.bak-questions`). Фид `watchlist.json` 106 867 Б и `movies-watchlist.md` 30 479 Б — оба mtime `02:32`. Vault git чистый, последний коммит 2026-09-23 14:47. ### 🔴 2026-09-23: `[x]` из Lampa НЕ проставляется — канал найден, но не подключён **Вопрос Alex:** «А у нас из лампы уходят отметки просмотрено в список галочками как было в jellyfin?» **Ответ: НЕТ.** Ни Lampa, ни Jellyfin сейчас не дают `[x]`. Задача в работе — см. [[personal/plans/lampac-watched-to-watchlist]]. **Почему Jellyfin-канал мёртв (замеры 2026-09-23):** | Проверка Jellyfin (`BaseItems` / `UserData`) | Значение | |---|---| | `BaseItems` всего | 53 | | из них `movie` | **0** | | из них `series` | 1 (плейлист/коллекция) | | `UserData.IsPlayed=true` | **0** | | `PlaybackActivity` | **0** | Причина: смотрят через **Lampa + TorrServer**, а не Jellyfin. Плюс с 2026-09-22 автозакачки выключены → новые файлы в Jellyfin не приходят. `sync_up.py` (`GET /Users/{id}/Items?IsPlayed=true` → `[x]`) читает пустоту. **🔑 Где реально живёт история просмотров — модуль Lampac `TimeCode`:** | Элемент | Значение | |---|---| | Модуль | `/lampac/module/Sync/TimeCode/` (`Controller.cs`, `plugin.js`, `SqlContext.cs`) | | БД | `/lampac/database/TimeCode.sql` (SQLite). **На хосте:** `$H/lampac/database/TimeCode.sql` | | Таблица | `timecodes` | | Колонки | `Id, user, identity, card, item, position, duration, percent, profile, deleted, watched_at, updated_at, extra` | | Пишет | Lampa → `POST /timecode/add` (легаси) / `/timecode/set` (нативный), пачками ≤10 req/s | | Ключ личности | `identity = {tmdb_id}_movie` — готовый ключ для матча со списком по TMDB-ID | Живая запись на 2026-09-23 (единственная в БД): ``` identity = 1758655_movie card = movie-1758655 position = 1222.0 duration = 6898.0 percent = 18.0 watched_at = 0 deleted = 0 ``` 🔴 **Питфолл: `sqlite3` в контейнере `lampac` НЕТ** (`sh: sqlite3: not found`). Читать БД так: `docker cp lampac:/lampac/database/TimeCode.sql /tmp/` + на хосте `python3 -c "import sqlite3; ..."`. На хосте Кра́кена `python3` есть. ⚠️ `viewed.json` и `config.db` в `/lampac/data/ts/` — это **TorrServer'овские**, к TS-истории просмотров Lampa отношения не имеют (не путать). **Решение (план, ждёт апрува):** новый модуль `sync_from_lampac.py` в `watchlist-sync` — читает `timecodes` (`deleted=0` и (`percent>=90` или `watched_at>0`)) → матч по `tmdb_id` из `watchlist.json` (у **346/346** films `tmdb_id` есть) → `[x]` через существующий `sync_up._mark_watched` → вызов в `watchlist-nightly.sh` после `sync-up`. Полный план, грабли и чек-лист приёмки — [[personal/plans/lampac-watched-to-watchlist]]. ### 🔜 2026-09-22: УДАЛЕНИЕ авто-закачек — инвентаризация снята, ЖДЁТ КОМАНДЫ Alex: *«закачки отменять и проверить еще раз что заново не добавятся»* → *«удалить все добавленные автоматикой закачки в т.ч. файлы. и radarr и sonarr»*. **Сделано:** полный дамп + разбор, что именно авто-добавлено. **Удаление НЕ запущено** — Alex не подтвердил список (16 позиций vs все 50). ``` дамп (единственный откат, корзины у Radarr нет): /tmp/radarr-sonarr-dump-20260922-184351/radarr-movies.json /tmp/radarr-sonarr-dump-20260922-184351/sonarr-series.json ``` **Критерий «авто»: `added >= 2026-09-20`** (окно работы `sync-down`; пайплайн watchlist'а включён в сентябре). Всё добавленное 18.05–04.06 в Radarr — ручное. | Система | Всего | Авто-добавлено | Комментарий | |---|---|---|---| | Radarr | 50 фильмов | **16** (12 с файлами, 4 без) | added 20–21.09 | | Sonarr | 13 сериалов | **0** | все 18.05–01.06, авто нет | Sonarr-список (для протокола, НЕ удалять): `Our Flag Means Death`, `Stranger Things`, `The Cyberfarm`, `Magical Police Department`, `Место встречи изменить нельзя`, `MOZU`, `Delicious in Dungeon`, `Rick and Morty`, `Severance`, `Brassic`, `Double or Quits`, `Futurama`, `True Detective`. 🔴 Полная таблица 16 Radarr-позиций с id и `hasFile` — в [[personal/tech/lampac-deployment#📋 Инвентаризация авто-закачек в Radarr/Sonarr (2026-09-22)]]. Там же — команда удаления и питфолл `deleteFiles=true` (**Radarr по умолчанию НЕ удаляет файл с диска**). **Гарантия «заново не добавятся» — проверена фактом (два независимых барьера):** ``` 1. sync-down вне крона → автозапуска нет 2. все 50 фильмов Radarr: monitored=false → поиск раздач не стартует + очередь (queue) 0 записей + wanted/missing 0 записей ``` ⚠️ **НЕ сделано:** коммит правки `watchlist-nightly.sh` в git (вырезанный `sync-down` живёт только на Кра́кене, в репо — старая версия). ### Правильные вызовы API (проверено) ```bash # 1. отмена загрузки — файлы на диске НЕ трогаются, blocklist НЕ трогать DELETE /api/v3/queue/?removeFromClient=true&blocklist=false # 2. снятие monitored — КЛЮЧ ПОЛЯ РАЗНЫЙ у Radarr и Sonarr! PUT /api/v3/movie/editor {"movieIds": [...], "monitored": false} # Radarr PUT /api/v3/series/editor {"seriesIds": [...], "monitored": false} # Sonarr, НЕ "ids" # 3. RSS off — отправлять ВЕСЬ объект конфига с id, иначе молча вернёт старое PUT /api/v3/config/indexer {"rssSyncInterval": 0, "id": 1, ...остальное} ``` Ключи API: Radarr `cbcb8ec320104359920c0f611d503c7b`, Sonarr `15fbec32f1cb4b05b1c2b229ce27359b` (из `` в `config.xml`). ### 🔴 Питфоллы этой операции 1. **`urllib` НЕ идёт по 307, а `curl -sL` — идёт.** Первый прогон: все 25 DELETE вернули `HTTP Error 307`, **ничего не удалилось**. Штатный `urllib` на 307/308 превращает DELETE в GET и теряет тело. Фикс — свой `HTTPRedirectHandler`, сохраняющий метод и тело (код в [[personal/tech/lampac-deployment]], раздел Radarr). 2. **Sonarr группирует эпизоды сезона под одним `id`** — после удаления первой записи остальные дают `404 Not Found`. Это **не сбой**, они ушли вместе с родительской. 3. **`PUT config/indexer` требует весь объект с `id`** — частичный `{"rssSyncInterval":0}` молча возвращает прежнее значение (Radarr понадобился второй заход). 4. **Кириллица в inline-`jq` внутри `ssh '...'`** снова сломала команду (`unexpected INVALID_CHARACTER`). Ключи объекта — латиницей. ### Откат ```bash curl -sL -X PUT ".../api/v3/movie/editor" -d '{"movieIds":[...],"monitored":true}' curl -sL -X PUT ".../api/v3/series/editor" -d '{"seriesIds":[...],"monitored":true}' curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1,...}' cp radarr.db.bak-20260922-123404 radarr.db && docker restart radarr ``` ## Полная цепочка ``` [вс 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) | **не скачивается** (фильтр в `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]` (просмотренные) с `⭐` игнорируются. Работает **из всех секций** (`## Фильмы`, `## Новинки`, `## Мультфильмы`, `## Аниме`). ## Правила отбора в `sync-down` (актуально с 2026-09-21) Приоритет определяется **двумя факторами**, а не позицией строки: | Условие | Поведение | |---------|-----------| | `⭐` (U+2B50) в строке фильма | **первыми**, фильтры рейтинга И ужасов **не применяются** | | ужас (`is_horror()`, genre_id=27) без `⭐` | **отсеивается**, с логом `[🧟] … horror, skipped` ⬅️ новое 2026-09-21 | | все остальные `[ ]` | **по порядку файла** (сверху вниз), рейтинг ≥ 6.0 | | рейтинг < 6.0 (включая `0.0`) | отсеивается, с логом `[−] … skipped` | | `[x]` | пропускается | | `❓` / `🗓️` (ambiguous) | пропускается, обрабатывается отдельно | ⚠️ **Стоп-кран по месту (2026-09-21):** перед любым добавлением проверяется `MIN_FREE_GB = 100`. Если свободно меньше — прогон останавливается (`[STOP] free space … — nothing queued`). Проверка через **`shutil.disk_usage`**, не `df`. Именно отсутствие этой проверки заполнило диск на 100% 2026-09-21 — см. [[family/plans/watchlist-disk-overflow-fix]]. **Фильтр ужасов работает по ЖАНРУ (`candidate.is_horror()`, TMDB genre_id=27), а не по эмодзи `🧟♀️`.** В dry-run 2026-09-21: **33 horror item(s) skipped**, 14 rating skipped. Результат для Alex: 41 фильм с `🧟♀️` (многие — свежие релизы 2025–2026) больше не качается. Обход — поставить `⭐` в строку фильма. ⚠️ **Порядок файла ≠ рейтинг.** До 2026-09-20 код сортировал по рейтингу TMDB и игнорировал позицию в списке — из-за этого в батч попадали фильмы «не из начала списка». Исправлено коммитом `ada47da`. ⚠️ **`--batch` снижен 5 → 2** (2026-09-21) после инцидента с диском. Меняется в `~/scripts/watchlist-nightly.sh` (root:root — править через `sudo cp`). ⚠️ **`⭐` = обход фильтров, а не смена сортировки.** Это принципиальная позиция 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 ``` **Как ставилось** (идемпотентный скрипт, бэкап перед установкой): ```bash crontab -l > ~/crontab-backup-$(date +%Y%m%d-%H%M%S).txt # бэкап crontab /tmp/crontab-new.txt # установка crontab -l # проверка ``` Бэкап: `~/crontab-backup-20260920-212155.txt` **Проверено после установки:** - `systemctl is-active cron` → `active` - `grep -c watchlist /var/spool/cron/crontabs/kraken` → **2** (обе строки подхвачены демоном) - Логи пишутся в `/tmp/` — **теряются при перезагрузке**. Перенести в постоянное место, если нужно сохранять историю. ⚠️ **В `docker exec hermes-kraken hermes cron list` по-прежнему «No scheduled jobs»** — это НЕ про watchlist. Watchlist-скрипты детерминированные, живут в системном cron, в Hermes-планировщике их не должно быть. Не искать их там. ## 🔴 2026-09-22: ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — баг и фикс **Симптом (сообщил Alex):** «Проект Конец света» показывался в каталоге Lampa как непросмотренный, хотя в главном списке он `[x]` (просмотрен) и был скачан. Alex предположил «опять синк не работает». **Корень — НЕ синк, а `process_thumbs()` в `parser.py`.** Регексп функции матчит только строки с плиткой: ```python _THUMBS_RE = r'^(\s*-\s*\[.\]\s*)(👍|👎)?\s*(.*?)\s*(👍|👎)?$' ``` Ветвление (актуальное поведение): - `👍` → строка **копируется** в начало `## Фильмы`, из `## Новинки` **удаляется** - `👎` → переносится в `## Новинки👎` - `[x]` → переносится в конец `[x]`-блока главного списка - **всё остальное (в т.ч. `⬇️` без плитки) → попадает в `keep` = остаётся в `## Новинки` НАВСЕГДА** **Осиротевшая строка в файле (была):** ``` - [ ] ⬇️ [Проект «Конец света»](kinopoisk/1382256/) (2026) IMDb 8.4 KP 8.4 ``` `⬇️` = «добавлен в Radarr», но плитки нет → ни под одно правило не подпадала → вечно `[ ]` в `## Новинки` → вечно в каталоге Lampa как непросмотренная. **Почему не нашёл поиском по названию:** написания разные — `Проект «Конец света»` (Новинки, со ссылкой KP) vs `Проект конец света` (главный список, `[x]`, без ссылки). ### Фикс 1 — `parser.py::process_thumbs()`: удаление сирот Добавлен блок: **собирает ключи всех `[x]`-строк из ВСЕХ секций** (кроме `## Новинки` и `## Новинки👎`), затем выкидывает из `keep` строки, чей ключ совпал. Сравнение по **двум** ключам: ссылка Кинопоиска (`kinopoisk/NNN/`) **или** нормализованное название. ```python # Нормализация: срезаем эмодзи, год, рейтинги, регистр, пробелы, «ё»→«е» _norm_title = lambda s: re.sub(r'[^а-яa-z0-9]+', '', re.sub(r'\b(?:IMDb|KP)\s*[\d.,]+', '', re.sub(r'\(?\b(19|20)\d{2}\b\)?', '', s.lower()))).replace('ё','е') ``` Результат: `Проект «Конец света» (2026) IMDb 8.4 KP 8.4` → `проектконецсвета`, `Проект конец света` → `проектконецсвета`. Совпадают. ⚠️ **Критично — снятие раннего `return []`.** Блок стоит ПОСЛЕ сбора `keep`, но в функции есть проверка `if not thumbs_up and not thumbs_down and not watched_items: return []`. Когда патч удаляет последнюю сироту, все три списка пусты → функция выходила **до записи файла**, и удаление терялось. Условие расширено флагом `orphans_removed`, чтобы запись состоялась. ### Фикс 2 — `cli.py`: корректный вывод действия `[drop]` попадал в `else`-ветку и печатался как перенос в «Новинки 👎» (косметика, но путает логи). Разведены явные ветки по типу действия. ### Проверка Прогон на **копии** боевого файла: удаляется **ровно 1** строка, 683 → 682, потерь нет. После прогона на боевом: каталог Lampa **404 → 403 позиции**. `## Новинки`: 34 → 33. «Конец света» отсутствует во всех 4 категориях. Бэкап перед правкой: `~/backup-watchlist-before-orphan-fix-20260922-113925.md` ### 🩹 ГЛАВНЫЕ ГРАБЛИ: patch-скрипты съедают обратные слэши (наступил 3 раза) При правке через write_file+python **двойное экранирование превращает escape-последовательности в сырые байты**. Синтаксис остаётся валидным, `ast.parse()` проходит, но **регекспы молча не работают**. | Написано | Осело в файл | Эффект | |---|---|---| | `r'\1'` | байт `\x01` | подстановка ссылки ломается | | `r'\b'` | байт `\x08` (backspace) | `\b` не матчит границу слова → рейтинги `IMDb 8.4 KP 8.4` не срезаются | **Как диагностировать:** ключ `проектконецсветаimdb84kp84` — рейтинги остались в ключе, значит `\b` мёртв. Проверять регекспы побайтово (`repr()` строки из файла), а не глазами через `read_file`. **Правило:** для regex-замен в этих скриптах использовать `chr(92)+'1'` / `chr(92)+'b'` либо проверять `repr()` после записи. Отлаживать через печать `repr()` реально загруженной строки. ### 📌 Следствие для будущего Ночной cron (01:00, `watchlist-nightly`) теперь **сам вычищает** такие сироты при каждом прогоне. Если в каталоге снова появится просмотренный фильм — первым делом `grep` по `## Новинки`. ## 🔴 Инцидент переполнения диска (2026-09-21) — историческая справка **Симптом:** диск `/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 отдавал `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 питфоллы