Files
obsidian-vault/family/plans/watchlist-disk-overflow-fix.md
T

14 KiB
Raw Blame History

title, created, updated, type, namespace, status, tags, related
title created updated type namespace status tags related
Watchlist — фикс переполнения диска и фильтра ужасов 2026-09-21 2026-09-21 plan family in_progress
watchlist
kraken
radarr
disk
fix
horror
rating
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 ГБ.

Корневая причина — три дефекта

# Дефект Где Следствие
1 Нет проверки свободного места перед добавлением в Radarr sync_down.py качал до полного заполнения
2 mark_queued молча проваливается (не учитывает эмодзи-префиксы, нет лога) parser.py:77 часть фильмов без метки ⬇️
3 Нет фильтра по ужасам (метка 🧟♀️ есть, но в sync_down не проверяется) 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

План — 6 фаз

Фаза 0 — Освободить место ✅ ВЫПОЛНЕНО

Удалены 5 remux-файлов (≈403 ГБ), Radarr пересканирован, monitored=false выставлен всем 5:

Фильм Размер Radarr id
Babylon (2022) 93 ГБ 37
Schindler's List (1993) 90 ГБ 18
Midsommar (2019) 86 ГБ 35
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 скачает файлы заново.

Все 5 фильмов были в watchlist (проверено): стр. 83 Babylon, 81 Midsommar, 82 Triangle, 396 «Список Шиндлера» (по-русски!), 107 Неистовый. ⚠️ Поиск латиницей schindler даёт ложно-отрицательный результат — «Список Шиндлера» записан по-русски.

Фаза 1 — MIN_FREE_GB (стоп-кран по месту) ✅ ВЫПОЛНЕНО

Файл: watchlist_sync/sync_down.py — коммит 2642882

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 до всех фильтров)
ужас (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 + watchlist_sync/sync_down.py, коммит 2642882

Было:

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] Неистовый
396:  - [ ] ⬇️ Список Шиндлера  →  - [ ] Список Шиндлера
677 строк — не изменилось

Бэкап: ~/backup-movies-watchlist-unmark-20260921-152018.md Тесты: unmark 5/5 + round-trip mark → unmark → исходная строка ✅

⚠️ unmark_queued в коде НЕ существовала (была только unmark_ambiguous). ⚠️ Не «восстанавливать» ⬇️ после удаления — метка означает «добавлен в Radarr»; после удаления файла + monitored=false она врёт и блокировала бы повторную постановку.

Фаза 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)

Метка Символ Кол-во строк Смысл
⭐ 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), выглядит как две метки, но это одна группа. Не путать с двумя отдельными маркерами.

Связанные заметки