From f9e8206b4dcfd27d7b3ffa728b717fbc7156fb0c Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 21 Sep 2026 15:24:01 +0600 Subject: [PATCH] [2026-09-21] eagle: family/plans/watchlist-disk-overflow-fix.md family/projects/watchlist-automation.md --- family/plans/watchlist-disk-overflow-fix.md | 129 +++++++++++++---- family/projects/watchlist-automation.md | 147 ++++++++++++++++++-- 2 files changed, 240 insertions(+), 36 deletions(-) diff --git a/family/plans/watchlist-disk-overflow-fix.md b/family/plans/watchlist-disk-overflow-fix.md index a43bbb17..ff45d754 100644 --- a/family/plans/watchlist-disk-overflow-fix.md +++ b/family/plans/watchlist-disk-overflow-fix.md @@ -24,12 +24,16 @@ related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access] | 2 | `mark_queued` молча проваливается (не учитывает эмодзи-префиксы, нет лога) | `parser.py:77` | часть фильмов без метки `⬇️` | | 3 | Нет фильтра по ужасам (метка `🧟♀️` есть, но в `sync_down` не проверяется) | `sync_down.py:54` | Midsommar и др. скачались | -## Текущее состояние (2026-09-21, после Фазы 0) +## Текущее состояние (2026-09-21) - Диск: **100% → 77%**, свободно **404 ГБ** - Radarr: ✅ API жив, 49 фильмов - Крон `watchlist-nightly` и `watchlist-discover`: **ЗАГЛУШЕНЫ** (бэкап `~/crontab-backup-20260921.txt`) -- Бэкапы: `~/backup-movies-watchlist-20260921-150759.md`, `~/backup-radarr-unmonitor-20260921-151117.json` +- Код (`sync_down.py`, `parser.py`): правки закоммичены, **`2642882`** +- Бэкапы: `~/backup-movies-watchlist-20260921-150759.md`, + `~/backup-radarr-unmonitor-20260921-151117.json`, + `~/backup-wlsync-20260921-151855/` (sync_down.py + parser.py до правок), + `~/backup-movies-watchlist-unmark-20260921-152018.md` ## План — 6 фаз @@ -45,30 +49,56 @@ related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access] | Unhinged (2020) | 57 ГБ | 4 | Папки НЕ удалены (там .nfo, постеры). +Скрипт удаления: `/tmp/rm_remux.sh` на Mac → `/tmp/rm_remux.sh` на Kraken (удалён при перезагрузке). +Снятие monitored: `/tmp/unmonitor_remux.sh`; порядок — `PUT monitored=false` ДО любого поиска, +иначе Radarr скачает файлы заново. -### Фаза 1 — `MIN_FREE_GB` (стоп-кран по месту) -**Файл:** `watchlist_sync/sync_down.py` -Перед добавлением в Radarr — проверка свободного места. +**Все 5 фильмов были в watchlist** (проверено): стр. 83 Babylon, 81 Midsommar, 82 Triangle, +396 «Список Шиндлера» (по-русски!), 107 Неистовый. ⚠️ Поиск латиницей `schindler` даёт +ложно-отрицательный результат — «Список Шиндлера» записан по-русски. + +### Фаза 1 — `MIN_FREE_GB` (стоп-кран по месту) ✅ ВЫПОЛНЕНО +**Файл:** `watchlist_sync/sync_down.py` — коммит `2642882` +```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). Порог: **100 ГБ** (согласован Alex). +Проверено dry-run: `free space on /home/kraken/obsidian/family/documents: 403.4 GiB (threshold 100 GiB)`. -### Фаза 2 — Фильтр ужасов + приоритет `⭐ > 🧟` -**Файл:** `watchlist_sync/sync_down.py:54` -Правило (симметрично существующему `⭐ > рейтинг`): +### Фаза 2 — Фильтр ужасов + приоритет `⭐ > 🧟` ✅ ВЫПОЛНЕНО +**Файл:** `watchlist_sync/sync_down.py` (фильтр перед проверкой рейтинга), коммит `2642882` | Условие | Действие | |---|---| -| есть `⭐` (U+2B50) | качаем **всегда** — обходит и рейтинг, и ужасы | -| ужас (`is_horror()`) без `⭐` | **пропускаем** | +| есть `⭐` (U+2B50) | качаем **всегда** — обходит и рейтинг, и ужасы (ветка `continue` до всех фильтров) | +| ужас (`candidate.is_horror()`) без `⭐` | **пропускаем** | | рейтинг < `MIN_RATING` (6.0) без `⭐` | пропускаем (как сейчас) | +Счётчик `skipped_horror`, в конце: `(N horror item(s) skipped; add ⭐ to force)`. +Проверено dry-run: **33 horror item(s) skipped**, 14 ниже рейтинга, `⭐ 1 priority item`. + +⚠️ `is_horror()` работает по **жанрам TMDB** (`genres` = список int-id, `27` = Horror), +не по эмодзи. Поэтому отсекается **больше** (33), чем помечено `🧟♀️` в списке (41, включая +те, что не резолвятся). В `.resolve_cache.json` **51 запись содержит genre id 27**. +Проверка: `jq -r "[to_entries[] | select((.value.genres // .value.candidate.genres // []) | index(27))] | length" .resolve_cache.json` + ⚠️ `⭐` (U+2B50) ≠ `✭` (U+272D). Это разные символы: `✭` — декоративная звёздочка в конце названия, срезается `_clean()`. Приоритет — только `⭐`. -### Фаза 3 — Фикс `mark_queued` -**Файл:** `watchlist_sync/parser.py:77` +### Фаза 3 — Фикс `mark_queued` ✅ ВЫПОЛНЕНО +**Файлы:** `watchlist_sync/parser.py` + `watchlist_sync/sync_down.py`, коммит `2642882` -Текущий код: +Было: ```python pattern = re.compile(r'(^\s*-\s*\[ \]\s*)(' + re.escape(title) + r')', re.MULTILINE) new_text = pattern.sub(r'\1⬇️ \2', text, count=1) @@ -76,31 +106,78 @@ if new_text != text: path.write_text(new_text) # ← тихий провал, если не совпало ``` -Дефекты: не учитывает эмодзи-маркеры (`🧟♀️`, `✭`) перед названием; не возвращает -результат; провал не логируется. +Стало: +- `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)` -Фикс: разрешить опциональный эмодзи-префикс; вернуть `bool`; в `sync_down.py` -(строки 98, 104, 113, 122) проверять результат и логировать провал. +**Тесты:** 7/7 прошли. Негативный случай (несуществующая строка) → `False`. +**Проверка на копии боевого файла:** 3 ранее падавшие строки (81, 82, 83) получили метку, +число строк 677 → 677 не изменилось. -### Фаза 4 — Снять `⬇️` у удалённых фильмов -- строка 396: `- [ ] ⬇️ Список Шиндлера` → `- [ ] Список Шиндлера` -- строка 107: `- [x] ⬇️ Неистовый` → `- [x] Неистовый` +### Фаза 4 — Снять `⬇️` у удалённых фильмов ✅ ВЫПОЛНЕНО +Добавлена `unmark_queued()` в `parser.py` (инверсия `mark_queued`, тоже возвращает `bool`, +тоже поглощает эмодзи-префикс с обеих сторон маркера). +Применено на боевом файле: +``` +107: - [x] ⬇️ Неистовый → - [x] Неистовый +396: - [ ] ⬇️ Список Шиндлера → - [ ] Список Шиндлера +677 строк — не изменилось +``` +Бэкап: `~/backup-movies-watchlist-unmark-20260921-152018.md` +**Тесты:** unmark 5/5 + round-trip `mark → unmark → исходная строка` ✅ -⚠️ `unmark_queued` в коде **НЕ существует** (есть только `unmark_ambiguous`). -Либо добавляем, либо правим точечно. -Метка `⬇️` = «добавлен в Radarr», после удаления файла + `monitored=false` она врёт. +⚠️ `unmark_queued` в коде **НЕ существовала** (была только `unmark_ambiguous`). +⚠️ **Не «восстанавливать» `⬇️` после удаления** — метка означает «добавлен в Radarr»; +после удаления файла + `monitored=false` она врёт и блокировала бы повторную постановку. -### Фаза 5 — Вернуть крон +### Фаза 5 — Вернуть крон ⏳ ОЖИДАЕТ ``` 0 1 * * * bash ~/scripts/watchlist-nightly.sh ``` С `--batch 2` вместо 5 (`watchlist-nightly.sh`). +Возвращать **только после проверки Фаз 1–2 в бою** (сначала dry-run). + +## 🔴 Открытые баги, найденные при работе (НЕ исправлены) + +Обнаружены в `watchlist_sync/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'` → после sub → `'🧟♀️ midsommar 2019'` (эмодзи остаётся в `title`). Лечится формой `^(?:[<символы>]+\s*)+`. | + +**Проверка A:** `_clean('⬇️ Babylon (2022)')` → `'Babylon (2022)'` (ожидалось `'Babylon'`). +**Проверка B:** `_EMOJI_PREFIX.match('⬇️ 🧟♀️ midsommar 2019').group(0)` → `'⬇️ '`, +остаток `'🧟♀️ midsommar 2019'`. + +**Почему это важно:** `item.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]`** +(Jellyfin → watchlist). Исправлено симметрично (локально `/tmp/wlsync/sync_up.py`), +тесты 5/6 (падение на `Babylon (2022)` — это баг A, не мой). +⚠️ **На Kraken НЕ залито, в git НЕ закоммичено.** + +⚠️ На момент правки строк с `⬇️` + эмодзи-префиксом в файле **не было ни одной** +(всего `⬇️` — 127, из них `[ ] ⬇️ ` — 59). Баг проявился бы **после первого боевого прогона** +`sync-down`, то есть моя правка Фазы 3 его бы создала. Поэтому чинить синхронно. ## Порядок и правила - Правки в боевых файлах: **коммит ДО → правка → заливка → ПРОВЕРКА АЛЕКСОМ → коммит ПОСЛЕ** - Проверка каждой фазы обязательна - Ничего не удалять без явного апрува +- **Не гадать про содержимое файла по одному grep** — «Список Шиндлера» по-русски + не нашёлся латиницей, из-за чего был сделан ложный вывод «фильма нет в списке» ## Факты по меткам (2026-09-21) @@ -109,10 +186,14 @@ if new_text != text: | `⭐` | U+2B50 | **1** («Простите за беспокойство ⭐ 2018») | приоритет, обходит все фильтры | | `✭` | U+272D | 4 | декоративная звёздочка в конце названия, НЕ приоритет | | `🧟♀️` | U+1F9DF U+200D U+2640 U+FE0F | **41** | ужасы | +| `⬇️` | U+2B07 U+FE0F | **127** (59 как `[ ] ⬇️ `) | добавлен в Radarr | ⚠️ Пересечений `⭐` + `🧟♀️` сейчас НЕТ. После включения фильтра **41 ужас перестанет качаться** (многие — свежие релизы 2025–2026). Решение Alex: **это ожидаемое поведение**. +⚠️ `🧟♀️` — **один составной эмодзи** (U+1F9DF + ZWJ + U+2640 + VS16), выглядит как две метки, +но это одна группа. Не путать с двумя отдельными маркерами. + ## Связанные заметки - [[watchlist-automation]] — полный flow pipeline - [[watchlist-cleanup]] — авто-удаление просмотренного diff --git a/family/projects/watchlist-automation.md b/family/projects/watchlist-automation.md index d8d63ead..08611433 100644 --- a/family/projects/watchlist-automation.md +++ b/family/projects/watchlist-automation.md @@ -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`, `Город бога`, `День хомячка`. Висят давно, релиз не находится/не качается.