[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

+105 -24
View File
@@ -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]] — авто-удаление просмотренного
+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`, `Город бога`, `День хомячка`. Висят давно, релиз не находится/не качается.