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
|
||||
type: plan
|
||||
namespace: family
|
||||
status: in_progress
|
||||
status: done
|
||||
tags: [watchlist, kraken, radarr, disk, fix, horror, rating]
|
||||
related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access]]"]
|
||||
---
|
||||
|
||||
# Watchlist — фикс переполнения диска и фильтра ужасов
|
||||
|
||||
> **Контекст:** 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 ГБ.
|
||||
> **Статус: ✅ ВЫПОЛНЕНО 2026-09-21.** Диск был 100% → 77% (404 ГБ). Все дефекты
|
||||
> починены и закоммичены (`2642882`, `a131208`). Крон возвращён с `--batch 2`.
|
||||
|
||||
## Корневая причина — три дефекта
|
||||
## Что произошло
|
||||
|
||||
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` | качал до полного заполнения |
|
||||
| 2 | `mark_queued` молча проваливается (не учитывает эмодзи-префиксы, нет лога) | `parser.py:77` | часть фильмов без метки `⬇️` |
|
||||
| 3 | Нет фильтра по ужасам (метка `🧟♀️` есть, но в `sync_down` не проверяется) | `sync_down.py:54` | Midsommar и др. скачались |
|
||||
| 1 | Нет проверки свободного места | `sync_down.py` | качал до полного заполнения |
|
||||
| 2 | `mark_queued` молча проваливался | `parser.py:77` | часть фильмов без `⬇️` |
|
||||
| 3 | Нет фильтра по ужасам | `sync_down.py:54` | Midsommar и др. скачались |
|
||||
|
||||
## Текущее состояние (2026-09-21)
|
||||
### Дополнительно найдены и починены (смежные)
|
||||
|
||||
- Диск: **100% → 77%**, свободно **404 ГБ**
|
||||
- Radarr: ✅ API жив, 49 фильмов
|
||||
- Крон `watchlist-nightly` и `watchlist-discover`: **ЗАГЛУШЕНЫ** (бэкап `~/crontab-backup-20260921.txt`)
|
||||
- Код (`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`
|
||||
| # | Дефект | Где | Следствие |
|
||||
|---|---|---|---|
|
||||
| 4 | `_EMOJI_PREFIX` брал только первый эмодзи из нескольких | `parser.py:24` | `title` = `'🧟♀️ midsommar 2019'` уходил в TMDB/Radarr |
|
||||
| 5 | Год срезался только в паре: `(?:/\d{4})+` вместо `*` | `parser.py:47` | `'Babylon (2022)'` не очищался |
|
||||
| 6 | `sync_up._mark_watched` не видел эмодзи-префикс | `sync_up.py` | фильм не переходил в `[x]` |
|
||||
|
||||
## План — 6 фаз
|
||||
---
|
||||
|
||||
### Фаза 0 — Освободить место ✅ ВЫПОЛНЕНО
|
||||
Удалены 5 remux-файлов (≈403 ГБ), Radarr пересканирован, `monitored=false` выставлен всем 5:
|
||||
## Результаты
|
||||
|
||||
### Фаза 0 — место освобождено ✅
|
||||
Удалены 5 remux (≈403 ГБ), Radarr пересканирован, `monitored=false`:
|
||||
|
||||
| Фильм | Размер | Radarr id |
|
||||
|---|---|---|
|
||||
@@ -48,151 +52,95 @@ related: ["[[watchlist-automation]]", "[[watchlist-cleanup]]", "[[kraken-access]
|
||||
| Triangle of Sadness (2022) | 80 ГБ | 36 |
|
||||
| Unhinged (2020) | 57 ГБ | 4 |
|
||||
|
||||
Папки НЕ удалены (там .nfo, постеры).
|
||||
Скрипт удаления: `/tmp/rm_remux.sh` на Mac → `/tmp/rm_remux.sh` на Kraken (удалён при перезагрузке).
|
||||
Снятие monitored: `/tmp/unmonitor_remux.sh`; порядок — `PUT monitored=false` ДО любого поиска,
|
||||
иначе Radarr скачает файлы заново.
|
||||
Папки НЕ удалены (там `.nfo`, постеры). Диск: **100% → 77%**.
|
||||
|
||||
**Все 5 фильмов были в watchlist** (проверено): стр. 83 Babylon, 81 Midsommar, 82 Triangle,
|
||||
396 «Список Шиндлера» (по-русски!), 107 Неистовый. ⚠️ Поиск латиницей `schindler` даёт
|
||||
ложно-отрицательный результат — «Список Шиндлера» записан по-русски.
|
||||
### Фаза 1 — стоп-кран по месту ✅
|
||||
`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` (стоп-кран по месту) ✅ ВЫПОЛНЕНО
|
||||
**Файл:** `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` (фильтр перед проверкой рейтинга), коммит `2642882`
|
||||
### Фаза 2 — фильтр ужасов + приоритет ⭐ ✅
|
||||
`sync_down.py:95` — перед добавлением проверяется `candidate.is_horror()`:
|
||||
|
||||
| Условие | Действие |
|
||||
|---|---|
|
||||
| есть `⭐` (U+2B50) | качаем **всегда** — обходит и рейтинг, и ужасы (ветка `continue` до всех фильтров) |
|
||||
| ужас (`candidate.is_horror()`) без `⭐` | **пропускаем** |
|
||||
| рейтинг < `MIN_RATING` (6.0) без `⭐` | пропускаем (как сейчас) |
|
||||
| есть `⭐` (U+2B50) | качаем **всегда** — обходит и рейтинг, и ужасы |
|
||||
| ужас без `⭐` | пропускаем |
|
||||
| рейтинг < 6.0 без `⭐` | пропускаем |
|
||||
|
||||
Счётчик `skipped_horror`, в конце: `(N horror item(s) skipped; add ⭐ to force)`.
|
||||
Проверено dry-run: **33 horror item(s) skipped**, 14 ниже рейтинга, `⭐ 1 priority item`.
|
||||
⚠️ `⭐` (U+2B50) ≠ `✭` (U+272D) — последний декоративный, срезается `_clean()`.
|
||||
В dry-run: **33 horror item(s) skipped**.
|
||||
|
||||
⚠️ `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`
|
||||
### Фаза 3 — надёжные метки ✅
|
||||
- `mark_queued` → возвращает `bool`, поглощает эмодзи-префикс, ставит метку **перед** ним
|
||||
- новая `unmark_queued` — снятие метки (её не существовало)
|
||||
- `_mark()` в `sync_down` логирует `[!] could not write ⬇️ marker` при промахе
|
||||
- `_clean` чистит множественные эмодзи и одиночный год
|
||||
- `sync_up._mark_watched` видит эмодзи-префикс
|
||||
|
||||
⚠️ `⭐` (U+2B50) ≠ `✭` (U+272D). Это разные символы: `✭` — декоративная звёздочка
|
||||
в конце названия, срезается `_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`,
|
||||
тоже поглощает эмодзи-префикс с обеих сторон маркера).
|
||||
Применено на боевом файле:
|
||||
### Фаза 4 — метки приведены в порядок ✅
|
||||
```
|
||||
107: - [x] ⬇️ Неистовый → - [x] Неистовый
|
||||
396: - [ ] ⬇️ Список Шиндлера → - [ ] Список Шиндлера
|
||||
677 строк — не изменилось
|
||||
```
|
||||
Бэкап: `~/backup-movies-watchlist-unmark-20260921-152018.md`
|
||||
**Тесты:** unmark 5/5 + round-trip `mark → unmark → исходная строка` ✅
|
||||
677 строк — не изменилось. `unmark_queued` вернул `True` оба раза.
|
||||
|
||||
⚠️ `unmark_queued` в коде **НЕ существовала** (была только `unmark_ambiguous`).
|
||||
⚠️ **Не «восстанавливать» `⬇️` после удаления** — метка означает «добавлен в Radarr»;
|
||||
после удаления файла + `monitored=false` она врёт и блокировала бы повторную постановку.
|
||||
|
||||
### Фаза 5 — Вернуть крон ⏳ ОЖИДАЕТ
|
||||
### Фаза 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`. |
|
||||
| **B** | `_EMOJI_PREFIX` = `^[<символы>]+\s*` — заканчивается на `\s*`, поэтому захватывает **только первый** эмодзи. | `parser.py:19-38` | `'⬇️ 🧟♀️ midsommar 2019'` → после sub → `'🧟♀️ midsommar 2019'` (эмодзи остаётся в `title`). Лечится формой `^(?:[<символы>]+\s*)+`. |
|
||||
| Тест | Результат |
|
||||
|---|---|
|
||||
| `_clean` unit | **11/11** |
|
||||
| `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 запрос),
|
||||
в `_normalize()` при `sync_up` (не сойдётся с названием из Jellyfin) и в `mark_queued`.
|
||||
Фикс B **частично компенсирован** правкой Фазы 3 (регексп поглощает префикс), но `title`
|
||||
всё равно уходит грязным в `resolve`.
|
||||
| SHA | Что |
|
||||
|---|---|
|
||||
| `2642882` | `MIN_FREE_GB` guard, horror filter, надёжный `mark_queued` |
|
||||
| `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/` |
|
||||
|
||||
## Порядок и правила
|
||||
## Открытые вопросы
|
||||
|
||||
- Правки в боевых файлах: **коммит ДО → правка → заливка → ПРОВЕРКА АЛЕКСОМ → коммит ПОСЛЕ**
|
||||
- Проверка каждой фазы обязательна
|
||||
- Ничего не удалять без явного апрува
|
||||
- **Не гадать про содержимое файла по одному grep** — «Список Шиндлера» по-русски
|
||||
не нашёлся латиницей, из-за чего был сделан ложный вывод «фильма нет в списке»
|
||||
1. **`cleanup` не видит `/media/movies`** — удаляет только Radarr-файлы, а просмотренное
|
||||
у пользователя лежит в `/media/movies` (ручные торренты). Пересечение пусто,
|
||||
поэтому cleanup пишет «nothing to delete» каждый день. **Не исправлено** — отдельная задача.
|
||||
2. **41 фильм с `🧟♀️`** перестанет качаться. Многие — свежие релизы 2025–2026.
|
||||
Решение Alex: ожидаемое поведение; для исключения ставить `⭐`.
|
||||
|
||||
## Факты по меткам (2026-09-21)
|
||||
## Уроки
|
||||
|
||||
| Метка | Символ | Кол-во строк | Смысл |
|
||||
|---|---|---|---|
|
||||
| `⭐` | 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), выглядит как две метки,
|
||||
но это одна группа. Не путать с двумя отдельными маркерами.
|
||||
- Скрипт, который качает данные, **обязан** иметь проверку свободного места — иначе
|
||||
он гарантированно заполнит диск.
|
||||
- Функция-мутатор файла, которая «молча ничего не делает» при промахе regex —
|
||||
источник тихой потери данных. Возвращать `bool` и логировать провал.
|
||||
- Regex на эмодзи-префиксы: `^[chars]+\s*` берёт **только первый** эмодзи.
|
||||
Нужно `^(?:[chars]+\s*)+` для повторяющихся групп.
|
||||
- Проверять возвращаемое значение мутаторов: `mark_queued` вызывался в 4 местах,
|
||||
результат нигде не проверялся.
|
||||
|
||||
## Связанные заметки
|
||||
- [[watchlist-automation]] — полный flow pipeline
|
||||
|
||||
Reference in new issue
Block a user