[2026-09-21] eagle: family/plans/watchlist-disk-overflow-fix.md family/projects/watchlist-automation.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-21 15:24:01 +06:00
1 parent a74d6691ec
commit f9e8206b4d
2 files changed
+240 -36

No files matched your search

+135 -12
View File
@@ -566,19 +566,139 @@ crontab -l | grep -c "watchlist-nightly\|watchlist-discover" # → 0 (пров
### 🔴 Незакрытые дефекты скрипта (моя ответственность)
| # | Дефект | Следствие |
|---|---|---|
| 1 | Нет проверки свободного места перед `sync-down` | Radarr качает, пока диск не кончится |
| 2 | Нет лимита размера файла | Один фильм = 60–100 ГБ |
| 3 | Нет стоп-крана при заполнении > порога | Нет автоматической остановки |
| 4 | `cleanup` не видит `/media/movies` | Просмотренное не удаляется, диск не освобождается |
**План ремонта (согласован):** сначала Фаза 1 — `MIN_FREE_GB` в скрипт (главный фикс), потом возврат крона с меньшим `--batch`. Cleanup — отдельно.
| # | Дефект | Следствие | Статус |
|---|---|---|---|
| 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, восстановлен** (детали в начале документа).
@@ -709,11 +829,14 @@ Jellyfin library mapping: Movies → `movies`, Cartoons → `mixed` (movies + ep
⚠️ Питфолл: `/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` без проверки места). Бэкап crontab: `~/crontab-backup-20260921.txt`. **Вернуть только после Фаз 1–2, с `--batch 2`.** Активен пока только `watchlist-cleanup` (02:00).
- 🔴 **[2026-09-21] ФАЗА 2 РЕМОНТА (главный фикс): `MIN_FREE_GB` в `sync-down`.** Стоп-кран: если свободно < порога — не добавлять в Radarr. Порог **не согласован** (предложение 100 ГБ, свободно 404). Без этого возврат крона бессмыслен. Лимит качества Alex считает уже настроенным — **новый не делать**.
- 🔴 **[2026-09-21] ФАЗА 1 (не начата): фикс `mark_queued` + `unmark_queued`.** `parser.py:77` — regex не учитывает эмодзи-префикс (`- [ ] 🧟‍♀️ midsommar 2019`), при провале **молча не пишет**. Нужно: опциональные маркеры в паттерне, возврат `bool`, проверка результата в `sync_down.py` (строки 98/104/113/122). Плюс `unmark_queued()` для снятия `⬇️` (у Шиндлера 396 и Неистового 107).
- 🔴 **[2026-09-21] ФАЗА 3 (не начата): фильтр ужасов + `⭐ > 🧟`.** Точка правки — `sync_down.py:54` (расширить существующий фильтр рейтинга). Правило Alex: ужас + `⭐` → качаем; ужас без `⭐` → пропускаем. ⚠️ **Под фильтр попадает 41 строка `🧟‍♀️`** (включая свежие релизы 2025–2026) — масштаб подтверждён Alex.
- ✅ **[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-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] ДИСК КРИТИЧЕН: 94.7% / свободно 4.8 ГБ (замер до полного заполнения).** Порог 90% пройден, cleanup реально запускается, но **удаляет 0 файлов**: пересечение `[x]` ∩ Radarr-библиотека = **пусто** (177 просмотренных vs 28 фильмов с файлами).
- 🟡 **[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`, `Город бога`, `День хомячка`. Висят давно, релиз не находится/не качается.