docs(watchlist): фикс диска 100% ЗАВЕРШЁН — все 5 фаз, 6 дефектов, верификация
This commit is contained in:
1 parent
f9e8206b4d
commit
8009a34e82
1 file changed
+88
-140
@@ -4,41 +4,45 @@ created: 2026-09-21
|
|||||||
updated: 2026-09-21
|
updated: 2026-09-21
|
||||||
type: plan
|
type: plan
|
||||||
namespace: family
|
namespace: family
|
||||||
status: in_progress
|
status: done
|
||||||
tags: [watchlist, kraken, radarr, disk, fix, horror, rating]
|
tags: [watchlist, kraken, radarr, disk, fix, horror, rating]
|
||||||
related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access]]"]
|
related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access]]"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Watchlist — фикс переполнения диска и фильтра ужасов
|
# Watchlist — фикс переполнения диска и фильтра ужасов
|
||||||
|
|
||||||
> **Контекст:** 2026-09-21 диск `/dev/sda1` на Kraken был заполнен на **100%** (0 байт свободно).
|
> **Статус: ✅ ВЫПОЛНЕНО 2026-09-21.** Диск был 100% → 77% (404 ГБ). Все дефекты
|
||||||
> Radarr отдавал `disk I/O error` (SQLite не мог писать). Причина — `watchlist-sync`
|
> починены и закоммичены (`2642882`, `a131208`). Крон возвращён с `--batch 2`.
|
||||||
> добавлял **5 фильмов ежедневно** (`sync-down --batch 5`) **без проверки свободного места**
|
|
||||||
> и **без отсечения ужасов**. Диск вырос 18 → 28 фильмов за сутки, remux'ы по 57–93 ГБ.
|
|
||||||
|
|
||||||
## Корневая причина — три дефекта
|
## Что произошло
|
||||||
|
|
||||||
|
2026-09-21 диск `/dev/sda1` на Kraken заполнился на **100%** (0 байт свободно).
|
||||||
|
Radarr отдавал `disk I/O error` — SQLite не мог писать. Причина: `watchlist-sync`
|
||||||
|
добавлял **5 фильмов ежедневно** (`sync-down --batch 5`) **без проверки места**
|
||||||
|
и **без отсечения ужасов**. За сутки 18 → 28 фильмов, remux'ы по 57–93 ГБ.
|
||||||
|
|
||||||
|
## Три корневых дефекта
|
||||||
|
|
||||||
| # | Дефект | Где | Следствие |
|
| # | Дефект | Где | Следствие |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | Нет проверки свободного места перед добавлением в Radarr | `sync_down.py` | качал до полного заполнения |
|
| 1 | Нет проверки свободного места | `sync_down.py` | качал до полного заполнения |
|
||||||
| 2 | `mark_queued` молча проваливается (не учитывает эмодзи-префиксы, нет лога) | `parser.py:77` | часть фильмов без метки `⬇️` |
|
| 2 | `mark_queued` молча проваливался | `parser.py:77` | часть фильмов без `⬇️` |
|
||||||
| 3 | Нет фильтра по ужасам (метка `🧟♀️` есть, но в `sync_down` не проверяется) | `sync_down.py:54` | Midsommar и др. скачались |
|
| 3 | Нет фильтра по ужасам | `sync_down.py:54` | Midsommar и др. скачались |
|
||||||
|
|
||||||
## Текущее состояние (2026-09-21)
|
### Дополнительно найдены и починены (смежные)
|
||||||
|
|
||||||
- Диск: **100% → 77%**, свободно **404 ГБ**
|
| # | Дефект | Где | Следствие |
|
||||||
- Radarr: ✅ API жив, 49 фильмов
|
|---|---|---|---|
|
||||||
- Крон `watchlist-nightly` и `watchlist-discover`: **ЗАГЛУШЕНЫ** (бэкап `~/crontab-backup-20260921.txt`)
|
| 4 | `_EMOJI_PREFIX` брал только первый эмодзи из нескольких | `parser.py:24` | `title` = `'🧟♀️ midsommar 2019'` уходил в TMDB/Radarr |
|
||||||
- Код (`sync_down.py`, `parser.py`): правки закоммичены, **`2642882`**
|
| 5 | Год срезался только в паре: `(?:/\d{4})+` вместо `*` | `parser.py:47` | `'Babylon (2022)'` не очищался |
|
||||||
- Бэкапы: `~/backup-movies-watchlist-20260921-150759.md`,
|
| 6 | `sync_up._mark_watched` не видел эмодзи-префикс | `sync_up.py` | фильм не переходил в `[x]` |
|
||||||
`~/backup-radarr-unmonitor-20260921-151117.json`,
|
|
||||||
`~/backup-wlsync-20260921-151855/` (sync_down.py + parser.py до правок),
|
|
||||||
`~/backup-movies-watchlist-unmark-20260921-152018.md`
|
|
||||||
|
|
||||||
## План — 6 фаз
|
---
|
||||||
|
|
||||||
### Фаза 0 — Освободить место ✅ ВЫПОЛНЕНО
|
## Результаты
|
||||||
Удалены 5 remux-файлов (≈403 ГБ), Radarr пересканирован, `monitored=false` выставлен всем 5:
|
|
||||||
|
### Фаза 0 — место освобождено ✅
|
||||||
|
Удалены 5 remux (≈403 ГБ), Radarr пересканирован, `monitored=false`:
|
||||||
|
|
||||||
| Фильм | Размер | Radarr id |
|
| Фильм | Размер | Radarr id |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -48,151 +52,95 @@ related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access]
|
|||||||
| Triangle of Sadness (2022) | 80 ГБ | 36 |
|
| Triangle of Sadness (2022) | 80 ГБ | 36 |
|
||||||
| Unhinged (2020) | 57 ГБ | 4 |
|
| Unhinged (2020) | 57 ГБ | 4 |
|
||||||
|
|
||||||
Папки НЕ удалены (там .nfo, постеры).
|
Папки НЕ удалены (там `.nfo`, постеры). Диск: **100% → 77%**.
|
||||||
Скрипт удаления: `/tmp/rm_remux.sh` на Mac → `/tmp/rm_remux.sh` на Kraken (удалён при перезагрузке).
|
|
||||||
Снятие monitored: `/tmp/unmonitor_remux.sh`; порядок — `PUT monitored=false` ДО любого поиска,
|
|
||||||
иначе Radarr скачает файлы заново.
|
|
||||||
|
|
||||||
**Все 5 фильмов были в watchlist** (проверено): стр. 83 Babylon, 81 Midsommar, 82 Triangle,
|
### Фаза 1 — стоп-кран по месту ✅
|
||||||
396 «Список Шиндлера» (по-русски!), 107 Неистовый. ⚠️ Поиск латиницей `schindler` даёт
|
`sync_down.py`: `MIN_FREE_GB = 100.0`. Проверка через **`shutil.disk_usage`**,
|
||||||
ложно-отрицательный результат — «Список Шиндлера» записан по-русски.
|
НЕ `df` (`df` учитывает root-резерв ext4 и врёт на ~4%).
|
||||||
|
Путь берётся из env `WATCHLIST_MEDIA_ROOT`, иначе — родитель watchlist-файла.
|
||||||
|
|
||||||
### Фаза 1 — `MIN_FREE_GB` (стоп-кран по месту) ✅ ВЫПОЛНЕНО
|
### Фаза 2 — фильтр ужасов + приоритет ⭐ ✅
|
||||||
**Файл:** `watchlist_sync/sync_down.py` — коммит `2642882`
|
`sync_down.py:95` — перед добавлением проверяется `candidate.is_horror()`:
|
||||||
```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` (фильтр перед проверкой рейтинга), коммит `2642882`
|
|
||||||
|
|
||||||
| Условие | Действие |
|
| Условие | Действие |
|
||||||
|---|---|
|
|---|---|
|
||||||
| есть `⭐` (U+2B50) | качаем **всегда** — обходит и рейтинг, и ужасы (ветка `continue` до всех фильтров) |
|
| есть `⭐` (U+2B50) | качаем **всегда** — обходит и рейтинг, и ужасы |
|
||||||
| ужас (`candidate.is_horror()`) без `⭐` | **пропускаем** |
|
| ужас без `⭐` | пропускаем |
|
||||||
| рейтинг < `MIN_RATING` (6.0) без `⭐` | пропускаем (как сейчас) |
|
| рейтинг < 6.0 без `⭐` | пропускаем |
|
||||||
|
|
||||||
Счётчик `skipped_horror`, в конце: `(N horror item(s) skipped; add ⭐ to force)`.
|
⚠️ `⭐` (U+2B50) ≠ `✭` (U+272D) — последний декоративный, срезается `_clean()`.
|
||||||
Проверено dry-run: **33 horror item(s) skipped**, 14 ниже рейтинга, `⭐ 1 priority item`.
|
В dry-run: **33 horror item(s) skipped**.
|
||||||
|
|
||||||
⚠️ `is_horror()` работает по **жанрам TMDB** (`genres` = список int-id, `27` = Horror),
|
### Фаза 3 — надёжные метки ✅
|
||||||
не по эмодзи. Поэтому отсекается **больше** (33), чем помечено `🧟♀️` в списке (41, включая
|
- `mark_queued` → возвращает `bool`, поглощает эмодзи-префикс, ставит метку **перед** ним
|
||||||
те, что не резолвятся). В `.resolve_cache.json` **51 запись содержит genre id 27**.
|
- новая `unmark_queued` — снятие метки (её не существовало)
|
||||||
Проверка: `jq -r "[to_entries[] | select((.value.genres // .value.candidate.genres // []) | index(27))] | length" .resolve_cache.json`
|
- `_mark()` в `sync_down` логирует `[!] could not write ⬇️ marker` при промахе
|
||||||
|
- `_clean` чистит множественные эмодзи и одиночный год
|
||||||
|
- `sync_up._mark_watched` видит эмодзи-префикс
|
||||||
|
|
||||||
⚠️ `⭐` (U+2B50) ≠ `✭` (U+272D). Это разные символы: `✭` — декоративная звёздочка
|
### Фаза 4 — метки приведены в порядок ✅
|
||||||
в конце названия, срезается `_clean()`. Приоритет — только `⭐`.
|
|
||||||
|
|
||||||
### Фаза 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)
|
|
||||||
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)`
|
|
||||||
|
|
||||||
**Тесты:** 7/7 прошли. Негативный случай (несуществующая строка) → `False`.
|
|
||||||
**Проверка на копии боевого файла:** 3 ранее падавшие строки (81, 82, 83) получили метку,
|
|
||||||
число строк 677 → 677 не изменилось.
|
|
||||||
|
|
||||||
### Фаза 4 — Снять `⬇️` у удалённых фильмов ✅ ВЫПОЛНЕНО
|
|
||||||
Добавлена `unmark_queued()` в `parser.py` (инверсия `mark_queued`, тоже возвращает `bool`,
|
|
||||||
тоже поглощает эмодзи-префикс с обеих сторон маркера).
|
|
||||||
Применено на боевом файле:
|
|
||||||
```
|
```
|
||||||
107: - [x] ⬇️ Неистовый → - [x] Неистовый
|
107: - [x] ⬇️ Неистовый → - [x] Неистовый
|
||||||
396: - [ ] ⬇️ Список Шиндлера → - [ ] Список Шиндлера
|
396: - [ ] ⬇️ Список Шиндлера → - [ ] Список Шиндлера
|
||||||
677 строк — не изменилось
|
|
||||||
```
|
```
|
||||||
Бэкап: `~/backup-movies-watchlist-unmark-20260921-152018.md`
|
677 строк — не изменилось. `unmark_queued` вернул `True` оба раза.
|
||||||
**Тесты:** unmark 5/5 + round-trip `mark → unmark → исходная строка` ✅
|
|
||||||
|
|
||||||
⚠️ `unmark_queued` в коде **НЕ существовала** (была только `unmark_ambiguous`).
|
### Фаза 5 — крон возвращён ✅
|
||||||
⚠️ **Не «восстанавливать» `⬇️` после удаления** — метка означает «добавлен в Radarr»;
|
|
||||||
после удаления файла + `monitored=false` она врёт и блокировала бы повторную постановку.
|
|
||||||
|
|
||||||
### Фаза 5 — Вернуть крон ⏳ ОЖИДАЕТ
|
|
||||||
```
|
```
|
||||||
0 1 * * * bash ~/scripts/watchlist-nightly.sh
|
0 1 * * * watchlist-nightly.sh # batch 5 → 2
|
||||||
|
0 9 * * 0 watchlist-discover.sh
|
||||||
|
0 2 * * * watchlist-cleanup.sh --apply
|
||||||
```
|
```
|
||||||
С `--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`. |
|
| `_clean` unit | **11/11** |
|
||||||
| **B** | `_EMOJI_PREFIX` = `^[<символы>]+\s*` — заканчивается на `\s*`, поэтому захватывает **только первый** эмодзи. | `parser.py:19-38` | `'⬇️ 🧟♀️ midsommar 2019'` → после sub → `'🧟♀️ midsommar 2019'` (эмодзи остаётся в `title`). Лечится формой `^(?:[<символы>]+\s*)+`. |
|
| `mark_queued` | **7/7** |
|
||||||
|
| `unmark_queued` + round-trip | **6/6** |
|
||||||
|
| `sync_up._mark_watched` | 5/6 (1 «фейл» — существующее поведение, не регрессия) |
|
||||||
|
| Парсинг боевого watchlist | 597 items, **0** title с мусором |
|
||||||
|
| dry-run `sync-down --batch 3` | 33 horror + 14 rating skipped, 3/3 queued, **ошибок нет** |
|
||||||
|
|
||||||
**Проверка A:** `_clean('⬇️ Babylon (2022)')` → `'Babylon (2022)'` (ожидалось `'Babylon'`).
|
## Коммиты
|
||||||
**Проверка B:** `_EMOJI_PREFIX.match('⬇️ 🧟♀️ midsommar 2019').group(0)` → `'⬇️ '`,
|
|
||||||
остаток `'🧟♀️ midsommar 2019'`.
|
|
||||||
|
|
||||||
**Почему это важно:** `item.title` с эмодзи/годом попадает в `resolve()` (TMDB/KP запрос),
|
| SHA | Что |
|
||||||
в `_normalize()` при `sync_up` (не сойдётся с названием из Jellyfin) и в `mark_queued`.
|
|---|---|
|
||||||
Фикс B **частично компенсирован** правкой Фазы 3 (регексп поглощает префикс), но `title`
|
| `2642882` | `MIN_FREE_GB` guard, horror filter, надёжный `mark_queued` |
|
||||||
всё равно уходит грязным в `resolve`.
|
| `a131208` | `_clean`: эмодзи-префиксы + одиночный год; `sync_up` эмодзи-префикс |
|
||||||
|
|
||||||
### Фаза 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 его бы создала. Поэтому чинить синхронно.
|
| watchlist (до удаления) | `~/backup-movies-watchlist-20260921-150759.md` |
|
||||||
|
| watchlist (перед снятием меток) | `~/backup-movies-watchlist-unmark-20260921-152018.md` |
|
||||||
|
| Radarr-состояние 5 фильмов | `~/backup-radarr-unmonitor-20260921-151117.json` |
|
||||||
|
| crontab | `~/crontab-backup-20260921-152741.txt` |
|
||||||
|
| `watchlist-nightly.sh` | `~/backup-watchlist-nightly-*.sh` |
|
||||||
|
| исходники `sync_down.py`/`parser.py` | `~/backup-wlsync-20260921-151855/` |
|
||||||
|
|
||||||
## Порядок и правила
|
## Открытые вопросы
|
||||||
|
|
||||||
- Правки в боевых файлах: **коммит ДО → правка → заливка → ПРОВЕРКА АЛЕКСОМ → коммит ПОСЛЕ**
|
1. **`cleanup` не видит `/media/movies`** — удаляет только Radarr-файлы, а просмотренное
|
||||||
- Проверка каждой фазы обязательна
|
у пользователя лежит в `/media/movies` (ручные торренты). Пересечение пусто,
|
||||||
- Ничего не удалять без явного апрува
|
поэтому cleanup пишет «nothing to delete» каждый день. **Не исправлено** — отдельная задача.
|
||||||
- **Не гадать про содержимое файла по одному grep** — «Список Шиндлера» по-русски
|
2. **41 фильм с `🧟♀️`** перестанет качаться. Многие — свежие релизы 2025–2026.
|
||||||
не нашёлся латиницей, из-за чего был сделан ложный вывод «фильма нет в списке»
|
Решение Alex: ожидаемое поведение; для исключения ставить `⭐`.
|
||||||
|
|
||||||
## Факты по меткам (2026-09-21)
|
## Уроки
|
||||||
|
|
||||||
| Метка | Символ | Кол-во строк | Смысл |
|
- Скрипт, который качает данные, **обязан** иметь проверку свободного места — иначе
|
||||||
|---|---|---|---|
|
он гарантированно заполнит диск.
|
||||||
| `⭐` | U+2B50 | **1** («Простите за беспокойство ⭐ 2018») | приоритет, обходит все фильтры |
|
- Функция-мутатор файла, которая «молча ничего не делает» при промахе regex —
|
||||||
| `✭` | U+272D | 4 | декоративная звёздочка в конце названия, НЕ приоритет |
|
источник тихой потери данных. Возвращать `bool` и логировать провал.
|
||||||
| `🧟♀️` | U+1F9DF U+200D U+2640 U+FE0F | **41** | ужасы |
|
- Regex на эмодзи-префиксы: `^[chars]+\s*` берёт **только первый** эмодзи.
|
||||||
| `⬇️` | U+2B07 U+FE0F | **127** (59 как `[ ] ⬇️ `) | добавлен в Radarr |
|
Нужно `^(?:[chars]+\s*)+` для повторяющихся групп.
|
||||||
|
- Проверять возвращаемое значение мутаторов: `mark_queued` вызывался в 4 местах,
|
||||||
⚠️ Пересечений `⭐` + `🧟♀️` сейчас НЕТ. После включения фильтра **41 ужас перестанет качаться**
|
результат нигде не проверялся.
|
||||||
(многие — свежие релизы 2025–2026). Решение Alex: **это ожидаемое поведение**.
|
|
||||||
|
|
||||||
⚠️ `🧟♀️` — **один составной эмодзи** (U+1F9DF + ZWJ + U+2640 + VS16), выглядит как две метки,
|
|
||||||
но это одна группа. Не путать с двумя отдельными маркерами.
|
|
||||||
|
|
||||||
## Связанные заметки
|
## Связанные заметки
|
||||||
- [[watchlist-automation]] — полный flow pipeline
|
- [[watchlist-automation]] — полный flow pipeline
|
||||||
|
|||||||
Reference in new issue
Block a user