[2026-09-22] eagle: family/projects/watchlist-automation.md personal/plans/lampac-watchlist-plugin.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-22 11:43:06 +06:00
1 parent 0edc4fe42f
commit e9566f840e
2 files changed
+873 -2

No files matched your search

+862 -1
View File
@@ -1,6 +1,6 @@
---
created: '2026-05-20'
updated: '2026-09-21'
updated: '2026-09-22'
status: active
tags:
- kraken
@@ -50,6 +50,12 @@ Jellyfin (8096) → rescan → виден контент
| `🧟‍♀️` | horror (genre_id=27) | **не скачивается** (фильтр в `sync-down` с 2026-09-21), если нет `⭐` |
| `⭐` | **приоритет к загрузке** (U+2B50) | sync-down берёт в первую очередь, **обходит ВСЕ фильтры** (рейтинг + ужасы) ✅ 2026-09-20/21 |
> 🔜 **С 2026-09-22 список читается ещё и в UI Lampa на ТВ** — непросмотренные `[ ]` отдаются
> каталогом «Мой список» через модуль `Catalog` в Lampac. ⭐ первыми, рейтинг ↓, ужасы последними.
> План: [[personal/plans/lampac-watchlist-plugin]].
> ⚠️ Отсюда требование: **`## Новинки` не должна содержать строк, чей фильм уже `[x]`** — иначе дубль
> в каталоге. Чистится автоматически `process-thumbs` (фикс 2026-09-22, см. ниже).
⚠️ **`✭` (U+272D) — НЕ приоритет.** Alex 2026-09-20 явно указал: звёздочкой-приоритетом считается только `⭐` (U+2B50). Символ `✭` используется в файле как обычная пометка «выделенное», приоритетом не является.
Также: `⭐` засчитывается **только в строке фильма** (`- [ ] Название ⭐ …`), НЕ в под-буллетах кандидатов, которые генерирует `mark_ambiguous` (там формат «Название (год) ★N.N»). `[x]` (просмотренные) с `⭐` игнорируются. Работает **из всех секций** (`## Фильмы`, `## Новинки`, `## Мультфильмы`, `## Аниме`).
@@ -125,6 +131,859 @@ crontab -l # проверка
⚠️ **В `docker exec hermes-kraken hermes cron list` по-прежнему «No scheduled jobs»** — это НЕ про watchlist. Watchlist-скрипты детерминированные, живут в системном cron, в Hermes-планировщике их не должно быть. Не искать их там.
## 🔴 2026-09-22: ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — баг и фикс
**Симптом (сообщил Alex):** «Проект Конец света» показывался в каталоге Lampa как непросмотренный,
хотя в главном списке он `[x]` (просмотрен) и был скачан.
**Корень — `process_thumbs()` в `parser.py`.**
Функция обрабатывает только три случая по регекспу
`_THUMBS_RE = r'^(\s*-\s*\[.\]\s*)(👍|👎)?\s*(.*?)\s*(👍|👎)?
**Симптом:** диск `/dev/sda1` на Kraken — **0 байт свободно** (`df` 100%). Radarr API отдавал
`disk I/O error` (SQLite не может писать при нуле места) — метаданные недоступны.
**Причина (НЕ cleanup):** `sync-down` в кроне `watchlist-nightly` (01:00) добавлял
**5 фильмов ежедневно** (`--batch 5`), **без проверки свободного места** и **без фильтра ужасов**.
Radarr качал remux'ы по 57–93 ГБ, пока диск не кончился. Профиль `WEB-4K (no remux)` фильтрует
**тип релиза**, но НЕ размер файла и НЕ проверяет место — от переполнения не защищает.
**Освобождено вручную:** удалены 5 remux = **403 ГБ** (Babylon 93 / Schindler's List 90 /
Midsommar 86 / Triangle of Sadness 80 / Unhinged 57 ГБ), всем выставлен `monitored=false`
через Radarr API. Диск: **100% → 77%** (404 ГБ).
**Починено в коде (коммиты `2642882`, `a131208`):**
| Что | Деталь |
|---|---|
| Стоп-кран по месту | `MIN_FREE_GB = 100.0` в `sync_down.py`, проверка через `shutil.disk_usage` (не `df`) |
| Фильтр ужасов | `candidate.is_horror()` (genre_id=27) пропускает ужасы, если нет `⭐` |
| `mark_queued` | возвращает `bool`, поглощает эмодзи-префикс, логирует промах через `_mark()` |
| `unmark_queued` | **новая функция** — снятие `⬇️` (не существовала) |
| `_clean` | чистит **множественные** эмодзи (`^(?:[chars]+\s*)+`) и **одиночный** год (`*` вместо `+`) |
| `sync_up._mark_watched` | видит эмодзи-префикс (иначе фильм не переходил в `[x]`) |
| `--batch` | **5 → 2** в `~/scripts/watchlist-nightly.sh` (root:root, править `sudo cp`) |
**Питфоллы, найденные в процессе:**
- `~/scripts/` на Kraken — **root:root 755**, `scp` падает с `Permission denied`. Только `sudo cp`.
- Крон-файлы вернуть можно только через `crontab /tmp/файл`; `crontab -l > f` для бэкапа обязателен.
- `du` по USB-HDD не влезает в SSH-таймаут на 100%-заполненном диске — запускать в фоне через `systemd-run`.
- Когда диск в нуле, **сначала освободить место**, только потом любые API-вызовы Radarr: SQLite мёртв.
**Открытая задача:** `cleanup` ([[family/plans/watchlist-cleanup]]) **не покрывает `/media/movies`** —
смотрит только Radarr-библиотеку, поэтому просмотренное из ручных торрентов не удаляется.
Пересечение `[x]` ∩ Radarr-файлы = 0.
**Полный разбор инцидента:** [[family/plans/watchlist-disk-overflow-fix]]
## 🔴 Аудит 2026-09-20: как выглядело состояние ДО восстановления (историческая справка)
**На момент аудита автодобавление фильмов НЕ работало по расписанию.** Скрипты были живы, но их никто не запускал.
Доказательства (проверено 2026-09-20):
- `crontab -l` юзера `kraken` — watchlist-строк **НЕТ**. Есть только `sync-vault.sh` (каждый час) и два `pgs-to-srt` (часовой + 03:00).
- root crontab и `/etc/cron.d/`: `grep -rl watchlist` → пусто.
- systemd timers (user + system) → 0 штук.
- Внутри Hermes-контейнера на Кракене: `docker exec hermes-kraken /opt/hermes/.venv/bin/hermes cron list` → **"No scheduled jobs."**
- Последний коммит автором `Hermes` (= nightly-скрипт) — **2026-06-20**. Распределение по месяцам: май 1, июнь 9, июль/август/сентябрь — **0**. Единственные коммиты в vault после июня — от `eagle`, `syncthing`, `taiga`.
Скрипты существуют и исполняемы, но не в кроне:
```
~/scripts/watchlist-nightly.sh mtime 2026-05-29
~/scripts/watchlist-discover.sh mtime 2026-05-29
```
> ⚠️ **Изначальная гипотеза «файл засран от многомесячного простоя» — НЕВЕРНА.** Дубли создавались на **каждом** прогоне `resolve` из-за бага в `mark_ambiguous()` (см. ниже, **исправлен**). 33 строки у «Nightmare Alley» = 11 прогонов. Простой лишь позволил мусору накопиться незамеченным. Мусор вычищен, баг закрыт.
## ✅ Баг дублирования кандидатов в `mark_ambiguous()` — НАЙДЕН И ИСПРАВЛЕН (2026-09-20)
### Симптом
Под каждой `❓`-строкой копились одинаковые дампы кандидатов. Рост ровно **+100 байт за прогон** (3 строки кандидатов) — подтверждено экспериментом: 26430 → 26530 → 26630 → 26730 байт.
### Корень (точная механика)
В `watchlist_sync/parser.py`, функция `mark_ambiguous()`:
```python
pattern = re.compile(
r'(^\s*-\s*\[ \]\s*)[❓]?\s*(' + escaped + r')\s*$', # ← матчит ТОЛЬКО строку ❓
re.MULTILINE
)
...
replacement = rf'\1❓ \2\n{cand_lines}' # ← новые кандидаты
new_text = pattern.sub(replacement, text, count=1) # ← ВСТАВЛЯЮТСЯ перед старыми
```
Паттерн снимал **только саму `❓`-строку**. Замена вставляла `\n` + 3 новых кандидата, а старые 33 строки оставались **ниже** — реплейс не знал о них. Итог: каждый прогон добавляет ещё один блок кандидатов поверх существующих.
**Важно:** `❓`-строки НЕ дублировались (каждая — одна). Дублировались **под-буллеты** под ними.
### Фикс
Паттерн теперь захватывает **весь блок**: строку-маркер ПЛЮС её под-буллеты, и перезаписывает целиком.
```python
# Candidate sub-bullet: indent of 4+ spaces, a bullet, NOT a checkbox line.
# Each iteration REQUIRES its own leading newline -- otherwise `*` walks onto
# the following line and swallows neighbouring items.
_SUB_BULLETS = r'(?:\n[ \t]{4,}-[ \t]*(?!\[[ xX]\])[^\n]*)'
pattern = re.compile(
r'(^[ \t]*-[ \t]*\[ \][ \t]*)[❓]?[ \t]*(' + escaped + r')[ \t]*'
r'(?P<subs>' + _SUB_BULLETS + r'*)',
re.MULTILINE
)
```
### ⚠️ Две НЕУДАЧНЫЕ версии паттерна (не повторять)
1. **v1** — `(?:\n[ \t]+-[^\n]*\n?)*`: `\n?` опционален, поэтому `*` перешагивал на следующую строку **без разделителя** и проглатывал соседние фильмы. Потеряны `❓ The kill room`, `The burial`, `Past lives`. Откачено.
2. **v2** — то же плюс негативный lookahead: не помогло, `[^\n]*` из предыдущей итерации уже захватил хвост следующей строки.
3. **v3 (рабочая)** — перенос обязателен внутри каждой итерации (`(?:` + `\n` + ...) + `*`), якорь не нужен.
**Правило для парсеров этого файла:** под-буллет кандидата = отступ **≥4 пробела**, это буллет и он **НЕ чекбокс** (`- [ ]` / `- [x]`). Строки фильмов имеют отступ 2 пробела — именно поэтому их надо исключать явно.
### Проверка фикса
На чистой копии (md5 `40295fac...`) — 3 прогона `mark_ambiguous()`:
```
run 1: size=25430
run 2: size=25430
run 3: size=25430
```
Размер стабилен. Потеряно **0** не-кандидатных строк. `Past lives` на месте. 838 → 808 строк (минус 30 = старые дубли, 33 → 3).
### Чистка существующих дублей
Скрипт `/tmp/wl_dedupe.py` (дедуп по `title|year|rating`, первое вхождение сохраняется, порядок не меняется; строки фильмов не трогаются). Прогон `--apply`:
- **168 дублей в 15 блоках**, 26430 → 21032 байт (838 → 670 строк).
- Топ: `Nightmare alley` 33→3, `Stand by me` 30→3, `Фарго` 30→3, `Left behind` 21→4, `Крестный отец` 12→3, `Godfathers` 12→3.
### Ручной прогон `watchlist-nightly.sh` (от юзера `kraken`)
```
bash ~/scripts/watchlist-nightly.sh
```
Прошёл до `Done`, без падений. Результат: **270 resolved (38 horror), 37 ambiguous, 22 not found**. `sync-down` добавил 4 фильма в Radarr (Американская история X, Мастерская сатаны, Я ругаюсь, Гладиатор), `Scream` уже был → помечен `⬇️`. `sync-up`: 7 played items из Jellyfin, 1 отмечен. Vault закоммичен и запушен (`Committed: family/documents/movies-watchlist.md`, `Pushed ✓`).
**После прогона дублей: 0** (проверено отдельным скриптом). Секции целы: Новинки / Фильмы / Мультфильмы / Аниме / Новинки👎 / Связанные заметки.
### 🔴 БАГ: `sync-down` игнорирует порядок файла — сортирует по рейтингу (найден 2026-09-20)
**Симптом (сообщил Alex):** в батч на закачку попали фильмы, которые «далеко не в начале списка» — `Гладиатор`, `Американская история X`, `Я ругаюсь`, `Мастерская Сатаны`.
**Корень:** `watchlist_sync/sync_down.py:41`:
```python
resolved.sort(key=lambda x: x[1].rating or 0, reverse=True)
```
Код **полностью игнорирует позицию строки в файле** и берёт топ-5 по рейтингу TMDB/KP из всех неотмеченных `[ ]`-строк. Порядок списка не имеет значения вообще.
**Расхождение с докой:** в [[watchlist-sync]] §1.3 заявлено «Приоритет: порядок в файле (выше = важнее)» — **это не соответствует коду**. Реальный приоритет = рейтинг.
**Вторая половина причины:** в том же прогоне `process-thumbs` выполняется **до** `sync-down` и переносит 👍-позиции из `## Новинки` в начало основного списка. Среди перенесённых оказались `Мастерская Сатаны` (8.3) и `Я ругаюсь` (8.2) — с высокими рейтингами, поэтому они сразу выиграли отбор.
**Как считался батч 2026-09-20 (5 слотов):**
| Слот | Фильм | Рейтинг | Итог |
|------|-------|---------|------|
| 1 | Крик / Scream | 10.0 | уже в Radarr → помечен `⬇️` |
| 2 | Волшебный участок | 9.0 | 400 «already added» в Sonarr |
| 3 | Гладиатор | 8.225 | **добавлен** |
| 4 | Я ругаюсь | 8.229 | **добавлен** |
| 5 | Мастерская Сатаны | 8.3 | **добавлен** |
| — | Американская история X | ~8.2 | **добавлен** (слот освободился после первых двух) |
`Saving Private Ryan` (8.223) и `Иди и смотри` (8.214) проиграли `Гладиатору` (8.225) на тысячные доли.
### ❌ В `sync_down` ОТСУТСТВУЕТ фильтр по рейтингу
Проверено `grep -rn "6\.0\|min_rating" watchlist_sync/*.py`:
- `cli.py:92` — `< 6.0` только для сброса кэша при `--recheck`, не отбор
- `discover.py:105,203` — фильтр только для шага discover
- `resolver.py:264` — `0.5` это сравнение рейтингов при ничьей, не порог
**В `sync_down` порога нет вообще.** Порог `min_rating: 6.0` из доки [[watchlist-sync]] §1.2 в коде не реализован. Итог: в очередь могут попасть фильмы с рейтингом 0.0-5.9.
**Замер 2026-09-20:** из 251 resolved-кандидатов **18 имеют рейтинг < 6.0** — например `Шестой элемент` 4.3, `Мегалополис` 5.2, `Бешеная любовь` 3.1, `Ворон 2024` 5.8.
⚠️ **Отдельный случай:** `rating == 0.0` означает «TMDB не дал оценку», а НЕ «плохой фильм». Таких двое: `Уроки благи`, `Годзилла и конг`. Наивный фильтр `< 6.0` их отсечёт и потеряет нормальное кино.
### ✅ РЕАЛИЗОВАНО (2026-09-20): `sync_down` следует порядку файла + фильтр `MIN_RATING`
План из «📋 ПЛАН» **выполнен**. Alex подтвердил: **порядок файла = приоритет, рейтинг = отбор**, нулевые рейтинги (`0.0`) **отсекать** вместе с остальными ниже порога.
**Коммиты в репо watchlist-sync (оба сделаны в этой сессии):**
```
ada47da feat(sync-down): follow file order, filter by MIN_RATING
2dab180 fix(parser): mark_ambiguous replaces candidate block instead of prepending
```
Предыдущий HEAD до сессии — `20ca511`. Рабочее дерево чистое.
**Правка 1 — убран `resolved.sort()`** (был на строке 41). `resolved` заполняется циклом по `parse()`, то есть остаётся в порядке файла сверху вниз.
**Правка 2 — добавлена константа и фильтр** в начало `sync_down.py`:
```python
# Minimum TMDB/KP rating for a title to be queued. 0.0 means "no rating in
# the database" -- per Alex it is discarded together with everything below
# the threshold, so unrated items never reach Radarr/Sonarr.
MIN_RATING = 6.0
```
```python
if result.status == "ok":
if (result.candidate.rating or 0) < MIN_RATING:
skipped_low += 1
print(f" [−] {item.title!r} — rating "
f"{result.candidate.rating or 0:.1f} < {MIN_RATING}, skipped")
continue
resolved.append((item, result.candidate))
else:
deferred.append((item, result))
```
Плюс итоговая строка `(N item(s) below rating 6.0 skipped)` — чтобы отсев был виден в логе, а не молчал.
**`process_thumbs` сознательно НЕ тронут** — 👍 по-прежнему уезжают в верх основного списка, то есть свежие новинки намеренно идут первыми (выбор Alex, «вариант 1»).
**Dry-run после правки (проверено):**
```
[−] ... 18 строк «skipped» (все < 6.0, включая два нуля: Уроки благи 0.0, Годзилла и конг 0.0)
(18 item(s) below rating 6.0 skipped)
[→] 'Под огнём' (2025) ★7.1 [dry-run]
[→] 'Секретный агент' (2025) ★7.3 [dry-run]
[→] 'Охота на воров 2: Пантера' (2025) ★6.6 [dry-run]
[→] 'Гадкая сестра' (2025) ★7.3 [dry-run]
[→] 'Семья в аренду' (2025) ★7.8 [dry-run]
Queued 5/5 items [dry-run]
```
Порядок ровно как в файле, с самого верха `## Новинки`. Рейтинг больше не влияет на очерёдность — только на отбор.
⚠️ **Реальный прогон (`bash ~/scripts/watchlist-nightly.sh`) после этой правки был ПРЕРВАН** — не отработал. Vault проверен: рабочее дерево чистое, `movies-watchlist.md` остался на коммите `d7ce0a9` (результат первого прогона), частичных изменений нет.
### ✅ ⭐ Приоритет по звёздочке `⭐` (U+2B50) — РЕАЛИЗОВАНО (2026-09-20)
**Требование Alex:** фильмы, отмеченные звёздочкой `⭐`, идут **в первую очередь** к загрузке — приоритет поверх порядка файла, **из всех секций** списка.
**Правила (финальные, подтверждены Alex):**
- Только символ **`⭐` (U+2B50)**. Символ `✭` (U+272D) приоритетом **НЕ является**.
- Только **строки фильмов** (`- [ ] Название ⭐ …`), НЕ под-буллеты кандидатов от `mark_ambiguous` («Название (год) ★N.N»).
- `[x]` (просмотренные) — **пропускаются**.
- **Из всех секций** — не только `## Фильмы`/`## Новинки` (уточнение Alex).
- ⭐ **обходит фильтр `MIN_RATING`** — «Я блядь сказал в приоритете завела». Звёздочка = явное «хочу», рейтинг не важен.
- `⭐` **срезается из поискового запроса** перед отправкой в TMDB (очевидное требование Alex).
**Коммит:** `7db9385 feat: ⭐ (U+2B50) items are queued first, bypassing the rating filter`
**Правка 1 — `parser.py::_clean()`.**
⚠️ **Найденный баг:** `⭐` стоит **в середине** названия (`Простите за беспокойство ⭐ 2018`), а старый regex был привязан к концу строки (`\s*[...]+\s*$`) — поэтому класс не матчился и символ утекал в запрос TMDB. Работало случайно (TMDB прощал лишний глиф).
```python
# ⭐ (U+2B50) is a priority marker and may sit ANYWHERE in the title
# ("Простите за беспокойство ⭐ 2018"), so strip it without the end anchor.
s = s.replace('⭐', ' ')
s = re.sub(r'\s+', ' ', s).strip()
s = re.sub(r'\s*[✭★☆✩✪✫✬✮✯✰]+\s*$', '', s).strip() # остальные звёзды — по-прежнему end-anchored
return s
```
**Критично:** `⭐` вырезается из `title` (чистый запрос), но **остаётся в `raw`** — приоритет проверяется по `item.raw`, поэтому оба свойства сохраняются.
**Правка 2 — `sync_down.py`, двухпроходная выборка:**
```python
# ⭐ (U+2B50) marks a must-have: such items are queued FIRST and bypass
# the rating filter entirely. Order within each group stays file order.
starred = [i for i in candidates_raw if "\u2b50" in i.raw]
rest = [i for i in candidates_raw if "\u2b50" not in i.raw]
if starred:
print(f" ⭐ {len(starred)} priority item(s) {[i.title for i in starred]}")
for item in starred + rest:
result = resolve(item.title, tmdb_key, kp_key)
if result.status == "ok":
if "\u2b50" not in item.raw and (result.candidate.rating or 0) < MIN_RATING:
skipped_low += 1
print(f" [−] {item.title!r} — rating "
f"{result.candidate.rating or 0:.1f} < {MIN_RATING}, skipped")
continue
resolved.append((item, result.candidate))
else:
deferred.append((item, result))
```
Обрати внимание на условие: `"\u2b50" not in item.raw and rating < MIN_RATING` — фильтр применяется **только** к не-⭐. Это и есть «обходит фильтр».
**Итоговые правила отбора в `sync-down`:**
| | ⭐ | обычные |
|---|---|---|
| Очерёдность | первыми | после, по порядку файла |
| Фильтр `MIN_RATING = 6.0` | **не применяется** | применяется |
| `[x]` | пропуск | пропуск |
**Проверка (2026-09-20):**
```
_clean('Простите за беспокойство ⭐ 2018') -> 'Простите за беспокойство 2018' (⭐ срезан)
raw: 'Простите за беспокойство ⭐ 2018' star in raw: True (приоритет работает)
title: 'Простите за беспокойство 2018' star in title: False (чистый запрос)
candidates: 250 | starred: 1 | rest: 249 — ⭐ первым в итерации
```
**Dry-run после правки:**
```
⭐ 1 priority item(s) ['Простите за беспокойство 2018']
[→] 'Простите за беспокойство 2018' → 'Простите за беспокойство' (movie, 2018, ★6.8) [dry-run]
[→] 'Гадкая сестра' (2025) ★7.3 ...
Queued 5/5 items [dry-run]
```
**Единственный ⭐ в файле** — строка 79, секция `## Фильмы`: `- [ ] Простите за беспокойство ⭐ 2018`.
### ✅ Реальный прогон 2026-09-20 19:53 — прошёл чисто
`bash ~/scripts/watchlist-nightly.sh` от юзера `kraken` → `Done`, ошибок нет. Вывод (`/tmp/wl-run.log`):
```
(18 item(s) below rating 6.0 skipped)
[=] 'Простите за беспокойство' already in Radarr, marking queued ← ⭐ обработан ПЕРВЫМ
[=] 'Гадкая сестра' already in Radarr, marking queued
[✅] 'Список заветных желаний' added to Radarr, search triggered
[✅] 'Отпуск на двоих' added to Radarr, search triggered
[✅] 'Простая случайность' added to Radarr, search triggered
Queued 5/5 items
[19:53:32] ▶ Done
```
⭐-фильм попал в очередь первым, но добавлять было нечего (уже в Radarr с прерванного прогона) — значит приоритет сработал. Дубли после прогона: **0**. Файл: 21332 байт / 676 строк.
### 🩹 Почему первая версия правки не сработала (не повторять)
Первая попытка добавить `⭐` в существующий класс `[✭★☆✩✪✫✬✮✯✰]` **не дала эффекта** — класс end-anchored (`$`), а `⭐` стоит в середине названия. Проверка на тестах показала `star gone: False`. Вывод: для маркеров, которые могут стоять в любой позиции, нужен `str.replace()`, а не класс с якорем.
### 📋 Как задача ставилась (для контекста будущих сессий)
Alex формулировал требования короткими репликами, и **план сначала был неверно истолкован дважды**:
1. «⭐ - в приоритете, из всех списков. Естественно в tmdb ее срезать» — уточнение, что секции не ограничены и глиф не должен уходить в запрос.
2. «Что блядь значит рейтинг остался?! Я блядь сказал в приоритете завела» — категорическое: ⭐ обходит фильтр рейтинга, а не просто меняет очерёдность.
**Вывод для будущего:** когда Alex говорит «в приоритете» — это означает **обход фильтров**, а не смену сортировки. Уточнять это ДО предложения плана, а не после.
### 📦 Результат закачки по итогам прогона 2026-09-20
`sync-down` добавил 4 фильма (`Scream` уже был → `⬇️`). Проверено через Radarr API + Transmission:
| Фильм | Взятый релиз | Статус |
|-------|--------------|--------|
| Gladiator (2000) | `UHD BDRip [H.265/2160p] [4K]` | качается |
| American History X (1998) | `BDRip [AVC]` | качается |
| I Swear (2025) / Я ругаюсь | `BDRip [H.264/1080p] [DVO]` | качается |
| Oficina do Diabo (2025) / Мастерская Сатаны | — | **релиз не найден**, ждёт RSS |
✅ **Профиль качества работает как задумано:** Gladiator взят в **4K, но `BDRip`, не Remux** — профиль 7 `WEB-4K (no remux)` фильтрует корректно.
**Застрявшие в очереди Radarr (со статусом `warning`, к этому прогону не относятся):** `Brokeback Mountain`, `Мортал Комбат 2`, `Sorry to Bother You`, `Город бога`, `День хомячка`. Отдельная задача — разобрать.
### 🟡 Побочная находка: Zulip-пинг не настроен на Кракене
`_zulip_ping()` требует `ZULIP_URL`, `ZULIP_API_KEY`, `ZULIP_BOT_EMAIL`, но их **нет в `.env`** репо watchlist-sync. При 59 нерезолвленных items → `[warn] ... not set — skipping ping`. Уведомления о нерезолвленных до Alex не доходят.
### 🟡 Побочная находка: Sonarr 400 «This series has already been added»
При `sync-down` для «Волшебный участок»: `400 Bad Request` на `/sonarr/api/v3/series`. Причина безобидная — **серия уже в Sonarr** (tvdbId=442038, «Magical Police Department»). `arr.py::sonarr_add()` не проверяет дубли перед POST, поэтому Sonarr отвечает `SeriesExistsValidator`. Не сбой, но шумит в логе — стоит добавить pre-check (как в `radarr_add`).
### Бэкапы (перед правками)
```
~/obsidian/family/documents/movies-watchlist.md.bak-20260920-192909 (838 строк, 26430 б)
~/obsidian/family/documents/movies-watchlist.md.dedupe-backup-20260920-193111
/srv/.../watchlist-sync/watchlist_sync/parser.py.bak-20260920-192909
```
### Статус на конец сессии (2026-09-20, финал)
**Три коммита в репо watchlist-sync** (порядок применения):
```
7db9385 feat: ⭐ (U+2B50) items are queued first, bypassing the rating filter
ada47da feat(sync-down): follow file order, filter by MIN_RATING
2dab180 fix(parser): mark_ambiguous replaces candidate block instead of prepending
20ca511 ← HEAD до сессии
```
Рабочее дерево чистое. `.bak-*` добавлены в `.gitignore`.
- ✅ Фикс `parser.py` (`mark_ambiguous`) применён, проверен 3 прогонами на чистой копии, закоммичен.
- ✅ Фикс `sync_down.py` — порядок файла + `MIN_RATING = 6.0`, закоммичен. Dry-run подтверждён.
- ✅ Приоритет `⭐` (U+2B50) **реализован**, закоммичен. Обходит фильтр рейтинга, работает из всех секций, `[x]` пропускает.
- ✅ Файл watchlist вычищен (168 дублей → 0) и закоммичен в vault.
- ✅ **Два реальных прогона `watchlist-nightly.sh` прошли чисто.** Второй (19:53) — уже с приоритетом ⭐: `Done`, ошибок нет, 3 фильма ушли в закачку.
- ✅ Дубли после всех прогонов: **0**.
- 🔴 **Крон по-прежнему отсутствует** — pipeline живёт только вручную. ГЛАВНАЯ незакрытая задача.
- 🟡 Zulip-пинг не настроен (`ZULIP_*` нет в `.env`).
- ℹ️ Доки [[watchlist-automation]] и [[watchlist-sync]] обновлялись агентом Eagle в этой же сессии (коммиты `bd743da`, `abee5a8` в vault) — при правках сверяться.
**Хронология прогонов 2026-09-20:**
| Время | Что | Результат |
|-------|-----|-----------|
| ~19:33 | Прогон после фикса дедупа (до правки `sync_down`) | 4 фильма в Radarr: Gladiator, American History X, I Swear, Oficina do Diabo (без релиза) |
| ~19:45 | Прогон после правки `sync_down` | **ПРЕРВАН** (прерван пользователем), правок не оставил |
| 19:53 | Финальный прогон (файловый порядок + `MIN_RATING` + ⭐) | `Done`. +3 фильма: Список заветных желаний, Отпуск на двоих, Простая случайность |
**Бэкапы (все созданы в этой сессии):**
```
~/obsidian/family/documents/movies-watchlist.md.bak-20260920-192909 (838 строк, 26430 б — ДО фикса)
~/obsidian/family/documents/movies-watchlist.md.dedupe-backup-20260920-193111 (до дедупа)
/srv/.../watchlist-sync/watchlist_sync/parser.py.bak-20260920-192909 (md5 0f597606fa14f357ff3b25547bd18ea6)
/srv/.../watchlist-sync/watchlist_sync/parser.py.bak-20260920-195100 (до правки ⭐)
/srv/.../watchlist-sync/watchlist_sync/sync_down.py.bak-20260920-194447
/srv/.../watchlist-sync/watchlist_sync/sync_down.py.bak-20260920-195100 (до правки ⭐)
```
**Скрипты диагностики/фикса (на Кракене, `/tmp/`):** `wl_dedupe.py` (дедуп кандидатов, `--apply`), `wl_postcheck.py` (проверка дублей после прогона), `wl_orphan.py` (проверка структуры блоков), `wl_verify_final.py`, `wl_order2.py`, `wl_grab_check.py` (Radarr queue + Transmission), `wl_fix_parser3.py`, `wl_fix_syncdown.py`, `wl_fix_star.py`, `wl_fix_clean2.py`, `wl_star.py`, `wl_star_check.py`, `wl_final_star.py`.
**Скрипты повторного использования:** `wl_dedupe.py` — при накоплении дублей; `wl_postcheck.py` — контроль после каждого прогона; `wl_grab_check.py` — что реально ушло в закачку.
### Пути к конфигам *arr (проверено 2026-09-20, в доке ранее были ошибочны)
Конфиги Radarr/Sonarr/Transmission лежат на **втором диске** (`6194539b-03e0-4f33-986b-4da09834e82f`), не на том же, что медиа:
```
Radarr API key: /srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config/config.xml
Sonarr: /srv/dev-disk-by-uuid-6194539b-.../docker/sonarr/config/config.xml
Transmission: /srv/dev-disk-by-uuid-6194539b-.../docker/transmission/settings.json
Radarr API base: http://localhost:7878/radarr/api/v3/ (ключ из config.xml)
Sonarr API base: http://localhost:8989/sonarr/api/v3/
```
Проверка закачки: `queue` в Radarr показывает `grabbed/downloading`; `history` даёт точное имя взятого релиза. Transmission — `docker exec transmission transmission-remote --list` (RPC-юзер в `settings.json` пустой).
## 🔴 2026-09-21: ДИСК ЗАПОЛНЕН 100%, КРОН `nightly`/`discover` ОСТАНОВЛЕН
**Причина остановки:** `sync-down --batch 5` (5 фильмов ежедневно) работал **без проверки свободного места**. Диск заполнился до **нуля байт**.
### Признак «сервисы ломаются» — Radarr отдаёт disk I/O error
```bash
curl -s "http://localhost:7878/radarr/api/v3/movie?apikey=$KEY"
# → {"message": "disk I/O error\ndisk I/O error",
# "description": "...System.Data.SQLite.SQLiteException (0x87AF045D): disk I/O error..."}
```
SQLite не может писать при нуле свободного места. **Метаданные Radarr через API не читаются, пока диск не освобождён.** Это первое, что надо проверять при «Radarr молчит» на заполненном диске.
### Фактическое заполнение (2026-09-21)
```
df -h → /dev/sda1 1.8T 1.7T 0 100% /srv/dev-disk-by-uuid-6194539b-...
df -h → /dev/mmcblk0p2 14G 4.7G 8.4G 36% / (SD-карта, не медиа-диск)
```
Docker: 29.4.3, Debian 12 bookworm, **arch `aarch64`**, 25 контейнеров, 22 образа.
### Что именно съело место — 5 REMUX-файлов = 403.4 ГБ
Замер `find -printf` по `/media/movies-radarr` (27 видеофайлов). **Настоящих remux'ов ровно 5** — по имени файла (`Remux`/`BDREMUX`/`BDRemux`):
| Размер | Фильм | Файл |
|---|---|---|
| 92.0 GB | Babylon (2022) | `Babylon (2022) - UHD BD-Remux by Wild_Cat.mkv` |
| 89.4 GB | Schindler's List (1993) | `Schindler's.List.1993.2160p.HDR.BDRemux.mkv` |
| 85.6 GB | Midsommar (2019) | `Midsommar.2019.DC.BDREMUX.2160p.HDR.seleZen.mkv` |
| 79.7 GB | Triangle of Sadness (2022) | `Triangle.of.Sadness.2022.BDREMUX.2160p.HDR.seleZen.mkv` |
| 56.7 GB | Unhinged (2020) | `Неистовый.2020.UHD.Blu-Ray.Remux.2160p.mkv` |
| **403.4 GB** | **итого** | |
⚠️ **Остальные 22 файла — НЕ remux** (WEB-DL / BDRip / x264 / x265, 4–29 ГБ). Крупные, но кодированные — не трогать. Например `Oldboy (2003)/Oldeuboi [2003].mkv` 28.6 ГБ и `Project.Hail.Mary...2160p.iT.WEB-DL.DV.HDR.H.265.mkv` 28.1 ГБ — **не remux**, несмотря на размер.
**Корень:** захардкоженный `qualityProfileId: 1` (профиль `Any`) в `arr.py` — починен только 2026-09-04. Всё, что качалось до этого, брало remux'ы по 60–98 ГБ.
### ✅ ИСПРАВЛЕНО 2026-09-21: ВСЕ 5 фильмов ЕСТЬ в watchlist
> ⚠️ **Ранее в этой доке стояло ошибочное «Schindler's List не найден в файле».** Это был **ложный вывод
> из одностороннего грепа**: фильм записан **по-русски** — «Список Шиндлера» (строка 396),
> а искали по латинице. Alex поправил.
> **Правило: искать и ru, и en — `grep -i "schindler\|шиндлер\|список"` — ПРЕЖДЕ чем утверждать «нет в списке».**
| Фильм | Строка в watchlist | Метка `⬇️` |
|---|---|---|
| Неистовый (Unhinged) | **107**: `- [x] ⬇️ Неистовый` | ✅ есть (и `[x]`) |
| Список Шиндлера | **396**: `- [ ] ⬇️ Список Шиндлера` | ✅ есть |
| Babylon (2022) | **83**: `- [ ] Babylon (2022)` | ❌ **потеряна** (баг `mark_queued`) |
| Midsommar | **81**: `- [ ] 🧟‍♀️ midsommar 2019` | ❌ **потеряна** |
| Triangle of Sadness | **82**: `- [ ] Triangle of Sadness 2022` | ❌ **потеряна** |
**Корень потери 3 меток — баг `mark_queued` (`parser.py:77`):** regex требует название **сразу** после `- [ ]`,
но перед ним бывает эмодзи (`- [ ] 🧟‍♀️ midsommar 2019`) → не матчит; при провале функция **молча ничего не пишет**
(`if new_text != text`), а `sync_down.py` её результат **не проверяет**. Детали — [[family/plans/watchlist-disk-overflow-fix]], Фаза 1.
### ❌ «Восстановить потерянные `⬇️`» — отменено (неверная постановка)
`⬇️` = «добавлен в Radarr / качается». После удаления файлов метка должна быть **снята**, а не восстановлена.
У Babylon/Midsommar/Triangle метки и не было → **ничего не делать** (консистентно: файла нет, метки нет).
Снять `⬇️` нужно у **Списка Шиндлера** (396 → `[ ]`) и **Неистового** (107 → `[x]`, `[x]` не трогать).
⚠️ Функции `unmark_queued()` в коде **НЕТ** — писать в рамках Фазы 1.
### ✅ Порядок работ — Фазы 0 и 0b ВЫПОЛНЕНЫ (2026-09-21)
1. ✅ **Удалены 5 remux-файлов** (403 ГБ) → `df` 100% → **77%**, свободно 404 ГБ → Radarr API ожил (49 фильмов).
Удаление — **файлами напрямую** (`rm` по точным путям), не через Radarr API (тот лежал). Папки с `.nfo`/постерами не тронуты.
2. ✅ **Radarr: `RescanMovie` + снятие `monitored`** у id 4/18/35/36/37 → все `monitored=false`, `hasFile=false`.
⚠️ **Без этого Radarr перекачал бы все 403 ГБ.** Порядок: `monitored=false` через PUT **до** любого поиска.
3. ✅ **Снят `⬇️`** у Шиндлера (396) и Неистового (107) — через новую `unmark_queued()`.
4. ✅ **`MIN_FREE_GB = 100.0` в `sync-down`** — согласован с Alex («Да»), коммит `2642882`.
5. ✅ Лимит качества Alex считает уже настроенным (`WEB-4K (no remux)`, id 7) — новый лимит не делали.
**Бэкапы:** `~/backup-movies-watchlist-20260921-150759.md`, `~/backup-radarr-unmonitor-20260921-151117.json`,
`~/crontab-backup-20260921.txt`. Скрипты: `/tmp/rm_remux.sh`, `/tmp/unmonitor_remux.sh`.
### Остановка крона (сделано 2026-09-21)
```bash
crontab -l > ~/crontab-backup-$(date +%Y%m%d-%H%M%S).txt # бэкап ПЕРЕД
crontab -l | grep -v "watchlist-nightly\|watchlist-discover" > /tmp/crontab-new.txt
crontab /tmp/crontab-new.txt
crontab -l | grep -c "watchlist-nightly\|watchlist-discover" # → 0 (проверка)
```
Бэкап: `~/crontab-backup-20260921.txt`.
**Осталось в кроне:** `sync-vault` (часовой), 2× `pgs-to-srt`, `watchlist-cleanup` (02:00).
**Убрано:** `watchlist-nightly` (01:00), `watchlist-discover` (вс 09:00).
### 🔴 Незакрытые дефекты скрипта (моя ответственность)
| # | Дефект | Следствие | Статус |
|---|---|---|---|
| 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, восстановлен** (детали в начале документа).
> ⚠️ **С 2026-09-21 `watchlist-nightly` и `watchlist-discover` ОСТАНОВЛЕНЫ** (диск 100%, см. секцию выше). Таблица ниже — целевое состояние; фактически активен только `watchlist-cleanup`.
| Job | Расписание | Скрипт | Факт 2026-09-21 |
|-----|-----------|--------|-----------------|
| `watchlist-nightly` | ежедн. 01:00 | `~/scripts/watchlist-nightly.sh` | ⏹ ОСТАНОВЛЕН |
| `watchlist-cleanup` | ежедн. 02:00 | `~/scripts/watchlist-cleanup.sh --apply` | ✅ активен |
| `watchlist-discover` | вс 09:00 | `~/scripts/watchlist-discover.sh` | ⏹ ОСТАНОВЛЕН |
⚠️ Фактический путь — `~/scripts/`, **не** `~/.hermes/scripts/`. Оба скрипта вызывают CLI напрямую через `uv run --project $WS`, без LLM. `uv` лежит в `~/.local/bin/uv`.
⚠️ **`~/scripts/` — root:root 755.** Добавить/заменить скрипт можно только `sudo install -m 755` (у kraken `sudo` без пароля). Обычная запись (`>`, `cp`) откажет с `Permission denied`.
⚠️ **`watchlist-cleanup.sh` без аргумента = dry-run.** В кроне обязателен `--apply`, иначе удаление не произойдёт и лог будет писать «nothing to delete». Это самая тихая из возможных поломок — проверять флаг при любой правке крона.
`WS` в обоих скриптах: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync` — **другой UUID**, чем фактический корень репо (см. ниже). Возможный источник поломки: проверить, что путь резолвится.
**watchlist-nightly.sh** (на Кракене):
1. `sync-vault.sh` (pull) → `process-thumbs --apply` → `sync-vault.sh` (push)
2. `resolve --recheck --apply` → `sync-down --apply --batch 5` → `sync-up --apply` → `sync-vault.sh` (push)
**Пути на Кракене (HDD UUID 49e8f586-3839-4c5d-a1e1-58bfc3579ade):**
- `/home/kraken/obsidian` → symlink на `/srv/dev-disk-by-uuid-.../obsidian`
- `/home/kraken/Developer/watchlist-sync` → symlink на `/srv/dev-disk-by-uuid-49e8f586-.../watchlist-sync`
✅ **Путь репо в скриптах ВЕРНЫЙ** (проверено 2026-09-20). В `watchlist-nightly.sh` и `watchlist-discover.sh`:
```
WS=/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync
UV=~/.local/bin/uv
```
`ls -la` подтверждает: `/srv/.../watchlist-sync` существует, внутри `watchlist_sync/`, `.venv/`, `.resolve_cache.json`, `pyproject.toml`, `.env` (есть, содержит TMDB_KEY/KP_KEY/RADARR_*/SONARR_*; **нет** ZULIP_*).
> Ранее в доке фигурировал путь `.../6194539b-.../watchlist-sync` «как реальный корень». На 2026-09-20 рабочий путь — **`49e8f586`**, и он же используется в скриптах. Не «исправлять» его обратно.
**Ещё одно подтверждённое отличие:** `~/scripts/` — **реальная директория** (`drwxr-xr-x root root`), а `~/.hermes/` на Кракане содержит **только** `skills/`. Скрипты лежат в `~/scripts/`, не в `~/.hermes/scripts/`.
### Как запускать CLI вручную (рабочая команда)
```bash
ssh kraken
cd /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync
uv run --project . watchlist-sync <команда> # resolve / sync-down / sync-up / process-thumbs / discover
```
Или целиком пайплайн: `bash ~/scripts/watchlist-nightly.sh`
## Discover — Фильтрация
| Тип | Порог |
|-----|-------|
| Мировые | IMDb ≥ 7.0 или KP ≥ 7.0 |
| Российские | KP ≥ 8.5 (защита от накрутки) |
| Upcoming | год ≥ текущий−1 |
| Минимум голосов | IMDb ≥ 1000 или KP ≥ 500 |
KP API endpoint: `https://api.poiskkino.dev/v1.4/movie` (redirect с api.kinopoisk.dev).
## *arr Stack — Индексеры (Prowlarr)
| ID | Имя | Тип | Примечание |
|----|-----|-----|-----------|
| 1 | RuTracker | semi-private | блокирует при rate limit на сутки |
| 2 | RuTor | public | поиск по imdbid |
| 3 | Byrutor | public | |
| 4 | NoNaMe Club | semi-private | анонимный |
| 5 | Kinozal | semi-private | резерв |
| 6 | Kinozal (M) | semi-private | магнет ✅ — без grab limit, **предпочтительный** |
**Pi-hole питфолл:** блокирует `skyhook.sonarr.tv` → Sonarr/Radarr поиск молча не запускается. Фикс: `dns: [8.8.8.8]` в docker-compose.yml.
## Router Script — Маршрутизация по жанру
После импорта Sonarr/Radarr: `router.py --apply` создаёт symlinks:
- Animation (не R/18+) → `/media/cartoons/` или `/media/cartoons-series/`
- Documentary → `/media/documentaries/` или `/media/documentaries-series/`
- Прочее → `/media/movies/` или `/media/series/`
Копирует NFO + постер рядом с symlink (Jellyfin не видит метадату через symlink напрямую).
## watchlist-sync — Python CLI
Репо: `~/Developer/watchlist-sync` (GitHub: `mallexxx/watchlist-sync`).
```
watchlist-sync resolve — TMDB + KP API → ID, тип (movie/tv)
watchlist-sync sync-down — добавить батч (max 5) в Radarr/Sonarr, поставить ⬇️
watchlist-sync sync-up — Jellyfin watched → [x] в .md
```
**Резолвер:** TMDB `/search/multi` + KP fallback. Пороги: `min_rating: 6.0`, `min_score: 0.75`. При 2+ кандидатах → `❓` (LLM разрешает в cron каждые 6ч, нерешённые → уточнение в Zulip).
**Stage 2 (после ≥50 просмотров):** AI-curated recommendations — три списка (с женой / один / семья) на основе TMDB профиля, добавляются в конец watchlist как `## 🤖 Рекомендации (YYYY-WNN)`.
Jellyfin library mapping: Movies → `movies`, Cartoons → `mixed` (movies + episodes).
## Питфоллы
### KP Markdown Link Parsing Bug
Фильмы из Kinopoisk с синтаксисом `[Title](https://kinopoisk.ru/film/12345/) (2025)` передавались как есть в TMDB search → не находились → помечались `❓` навсегда (`not i.ambiguous` пропускает при следующем запуске). Фикс: commit `2db1ed9` — извлекать plain title из markdown link до передачи в TMDB. Ошибочно помеченные `❓` требуют ручного `--recheck`.
### KP API Endpoint
`api.kinopoisk.dev` редиректит на `api.poiskkino.dev/v1.4/movie`. Использовать destination redirect напрямую.
### 🔴 Жёстко зашитый `qualityProfileId` в arr.py (корень жирных remux) — РЕШЕНО
Найдено 2026-09-04. В `watchlist_sync/arr.py` изначально был захардкожен **`qualityProfileId: 1`** и для Radarr, и для Sonarr:
```python
# radarr_add (~стр. 77) / sonarr_add (~стр. 112)
"qualityProfileId": 1,
```
Профиль 1 (Radarr) = **'Any'** — разрешает ВСЁ вплоть до Remux-2160p и BR-DISK, поэтому каждый новый фильм/сериал попадал в «Any» и Radarr скачивал 60GB remux'ы.
✅ **Решение принято и применено 2026-09-04:** создан профиль **`WEB-4K (no remux)`** → Radarr id **7**, Sonarr id **7**; `arr.py` теперь пишет `qualityProfileId: 7`. Подробности профиля — в [[arr-stack-kraken]], секция «Качество загрузки для НОВЫХ фильмов/сериалов».
### ✅ Баг накопления кандидатов в `mark_ambiguous()` (найден и исправлен 2026-09-20)
Кандидаты-под-буллеты под `❓`-строками дублировались **на каждом прогоне** `resolve` (+3 строки за раз), потому что `re.sub` снимал только саму `❓`-строку, а новые кандидаты вставлялись **перед** старыми. Паттерн исправлен на захват всего блока (маркер + под-буллеты). Полная механика, две неудачные версии паттерна и чистка 168 дублей — в [[family/projects/watchlist-automation]].
**Правило для парсеров watchlist:** под-буллет кандидата = отступ **≥4 пробела** + буллет + **НЕ чекбокс** (`- [ ]`/`- [x]`). Строки фильмов имеют отступ 2 пробела. Внутри `*`-группы перенос строки обязателен — иначе regex проглатывает соседние фильмы.
## Open Tasks
- ✅ **[2026-09-20] Фикс `sync_down.py` РЕАЛИЗОВАН** — коммит `ada47da`. `resolved.sort()` убран, добавлен `MIN_RATING = 6.0` с видимым `print`. Нулевые рейтинги отсекаются (решение Alex: «отсекать»).
- ✅ **[2026-09-20] Приоритет `⭐` (U+2B50) РЕАЛИЗОВАН** — коммит `7db9385`. ⭐ первыми из всех секций, **обходит** фильтр `MIN_RATING`. `⭐` срезается из запроса TMDB (`parser._clean` через `str.replace`, НЕ через end-anchored класс). `✭` (U+272D) приоритетом не считается; `[x]` пропускается; под-буллеты кандидатов не считаются.
- ✅ **[2026-09-20] Реальный прогон с новой логикой выполнен** — 19:53, `Done`, +3 фильма. Батч по порядку файла, ⭐ первым.
- ✅ **[2026-09-20] Крон ВОССТАНОВЛЕН** — установлен 21:21 в crontab юзера `kraken`, бэкап `~/crontab-backup-20260920-212155.txt`. `nightly` 01:00 + `discover` вс 09:00, логи в `/tmp/`. Первая проверка в бою — прогон в 01:00.
- 🟡 **[2026-09-20] Логи крона в `/tmp/`** — теряются при перезагрузке. Перенести в постоянное место (`~/logs/` или на диск), если нужна история прогонов.
- ✅ **[2026-09-21] Удаление просмотренных фильмов при заполнении диска >90% — ЗАКРЫТО И В КРОНЕ.** Обёртка `~/scripts/watchlist-cleanup.sh` (была в `/tmp` — перенесена через `sudo install`, у kraken sudo **без пароля**), cron `0 2 * * * ... --apply`, лог `/home/kraken/logs/watchlist-cleanup.log`. **Корзина Radarr (`recycleBin`) — решено НЕ настраивать** (контейнерный/хостовый пути расходятся; Alex больше не переспрашивать). Обёртка по умолчанию делает **dry-run** — без `--apply` в кроне был бы вечный холостой ход. Полные детали — [[family/plans/watchlist-cleanup]].
⚠️ Питфолл: `/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` без проверки места). **Возвращены в тот же день** после Фаз 1–4, уже с **`--batch 2`**. Бэкапы crontab: `~/crontab-backup-20260921.txt` (до), `~/crontab-backup-20260921-152741.txt` (после). Итоговый crontab:
```
0 1 * * * watchlist-nightly.sh # batch 2
0 9 * * 0 watchlist-discover.sh
0 2 * * * watchlist-cleanup.sh --apply
```
- ✅ **[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-22] ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — ИСПРАВЛЕНО.** Симптом Alex: «Проект Конец света» в каталоге Lampa как непросмотренный, хотя в главном списке он `[x]`. Корень: `process_thumbs` обрабатывает только `👍`/`👎`/`[x]`; строка `- [ ] ⬇️ [Название](kp) (2025) IMDb 7.1` (без оценки) уходила в `keep` **навсегда**, а при наличии `[x]`-двойника давала дубль в каталоге. **Фикс:** сбор `watched_titles`/`watched_kp` по всем секциям кроме Новинок + удаление сирот из `keep`; нормализация **обязана срезать год и рейтинги**, иначе `проектконецсвета` ≠ `проектконецсвета2026imdb84kp84`. Флаг `_patch_dropped` добавлен в ранний выход (иначе запись файла пропускалась). `cli.py` — явные ветки вывода `👍/👎/[x]/drop`. Проверено: удаляется ровно 1 строка, 683 → 682; каталог 404 → 403. Бэкапы: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`, код — `parser.py.bak-orphan-*`, `.bak-guard-*`, `.bak-bs-*`, `cli.py.bak-cli-*`. Полный разбор — секция «ОСИРОТЕВШИЕ СТРОКИ» выше; связь с каталогом Lampa — [[personal/plans/lampac-watchlist-plugin]].
- 🔴 **[2026-09-22] ПИТФОЛЛ правки `parser.py` скриптами: двойное экранирование съедает слэши.** `r'\1'` → реальный байт `\x01`, `r'\b'` → байт `\x08` (backspace). `compile()`/`ast.parse()` проходят — синтаксис валиден, а регекспы **молча не работают** (нормализация не срезала рейтинги, сироты не находились). **Правило: после записи патча проверять файл на `chr(1)` и `chr(8)`, а не только на синтаксис.** По возможности править через `patch`/Obsidian, а не генератором строк.
- 🔴 **[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] 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`, `Город бога`, `День хомячка`. Висят давно, релиз не находится/не качается.
- 🟡 **[2026-09-20] `Oficina do Diabo` / `Мастерская Сатаны` (2025)** — в Radarr, но релиз не найден. Ждёт RSS. Проверить через несколько дней.
- 🟡 **[2026-09-20] Настроить Zulip-пинг** — `ZULIP_URL` / `ZULIP_API_KEY` / `ZULIP_BOT_EMAIL` отсутствуют в `.env` watchlist-sync, 59 нерезолвленных items ушли в никуда без уведомления.
- 🟡 **[2026-09-20] Добавить pre-check дублей в `sonarr_add()`** — сейчас при уже существующей серии прилетает `400 SeriesExistsValidator` и шумит в логе. В `radarr_add()` проверка есть.
- 🟡 **[2026-09-20] Синхронизировать документацию с кодом** — [[watchlist-sync]] §1.2 и §1.3 описывают порог `min_rating: 6.0` и «приоритет = порядок в файле», которых в коде не было. Порог теперь реализован (`MIN_RATING`), приоритет по порядку тоже, плюс появился приоритет ⭐ — обновить формулировки в [[watchlist-sync]].
- 🔴 **[2026-09-04] Решить стратегию качества загрузки новых фильмов** — уже РЕШЕНО 2026-09-04 (профиль `WEB-4K (no remux)`, id 7 для Radarr и Sonarr). Осталось только убедиться, что старые `⬇️`-позиции, добавленные до фикса, не тянут remux.
- 🟡 Оставшиеся `❓`-позиции (71 маркер) — **намеренно не чистятся вручную** (решение Alex 2026-09-20). Разбираются через `resolve --recheck` при работающем кроне.
- 🟡 Передавать Transmission locations через router script при каждом импорте (сейчас вручную)
- 🔴 rsync невыполненных фильмов с HTPC на Кракен (`Фильмы не перенесенные с htpc на кракен.md`)
- 🟡 Watchlist sync-up: добавить перенесённые фильмы в нужные разделы
## Связанные заметки
- [[watchlist-discover]] — шаг discover: KP API, format, CLI команды
- [[watchlist-sync]] — основной проект (resolve, sync-down, sync-up)
- [[arr-stack-kraken]] — детальные питфоллы *arr стека
- [[jellyfin-transcode-rpi5]] — Jellyfin PGS/ASS питфоллы
`:
| Строка | Что делает |
|---|---|
| `👍` | копирует в начало главного списка, из Новинок **удаляет** |
| `👎` | переносит в `## Новинки👎` |
| `[x]` | переносит в конец `[x]` главного списка |
| **всё остальное** | **остаётся в `keep` = навсегда в `## Новинки`** |
Строка вида `- [ ] ⬇️ [Название](kp) (2025) IMDb 7.1` **не имеет ни 👍, ни 👎** → попадает в `keep`.
Если тот же фильм уже `[x]` в главном списке — он висит в Новинках как непросмотренный.
В каталоге Lampa это **дубль**.
**Фикс — два блока в `parser.py`, оба в `process_thumbs()`:**
1. **Сбор просмотренного** — обход всех строк файла, исключая `## Новинки` и `## Новинки👎`:
```python
watched_titles = set() # нормализованные названия [x]-строк
watched_kp = set() # KP-id из [x]-строк
```
Нормализация (`_norm_title`) **обязана срезать год и рейтинги** — иначе ключи не совпадут:
```python
s = re.sub(r'\[([^\]]+)\]\([^)]*\)', r'\1', s) # ссылка -> текст
s = re.sub(r'\(\s*\d{4}(?:\s*/\s*\d{4})*\s*\)', ' ', s) # (2025), (2006/2020)
s = re.sub(r'\b(?:IMDb|KP)\s*[\d.,]+', ' ', s, flags=re.I) # IMDb 8.4 KP 8.4
s = re.sub(r'[^0-9a-zа-яё]+', '', s.lower(), flags=re.I)
```
⚠️ Без среза года/рейтингов в главном списке ключ `проектконецсвета`, а в Новинках
`проектконецсвета2026imdb84kp84` — **не совпадают**.
2. **Удаление сирот из `keep`** — по `watched_kp` (точное) или `watched_titles` (нормализованное),
с записью в `actions` как `('[drop] уже просмотрено в другой секции', ...)`.
3. **Флаг `_patch_dropped`** — ранний выход `if not thumbs_up and not thumbs_down and not watched_items: return []`
**пропускал запись файла**, даже когда сироты найдены. Добавлено `and not _patch_dropped`.
**`cli.py`:** вывод `[drop]` был неотличим от `👎` (обобщённый `else`) → явные ветки
`👍 / 👎 / [x] / drop`.
**Проверка (2026-09-22):** на копии боевого файла — удаляется **ровно 1 строка**, 683 → 682,
«Конец света» пропадает из всех 4 категорий каталога (404 → 403 позиции).
Бэкап до правки: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`.
Бэкапы кода: `parser.py.bak-orphan-*`, `.bak-guard-*`, `.bak-bs-*`; `cli.py.bak-cli-*`.
### 🔴 Питфолл при правке `parser.py` скриптами
**Двойное экранирование в patch-скриптах съедает обратные слэши.** Дважды получил битые байты:
- `r'\1'` → **реальный байт `\x01`** в файле
- `r'\b'` → **реальный байт `\x08`** (backspace)
Оба раза `compile()` проходил (валидный питон), но регекспы молча не работали.
**Правило: после записи патча проверять файл на байты `chr(1)` и `chr(8)`,**
а не только на синтаксис. Либо править через Obsidian-patch напрямую, а не генератором строк.
## 🔴 2026-09-21: ДИСК 100% — инцидент и починка `sync-down`
**Симптом:** диск `/dev/sda1` на Kraken — **0 байт свободно** (`df` 100%). Radarr API отдавал
@@ -890,6 +1749,8 @@ Jellyfin library mapping: Movies → `movies`, Cartoons → `mixed` (movies + ep
- ✅ **[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-22] ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — ИСПРАВЛЕНО.** Симптом Alex: «Проект Конец света» в каталоге Lampa как непросмотренный, хотя в главном списке он `[x]`. Корень: `process_thumbs` обрабатывает только `👍`/`👎`/`[x]`; строка `- [ ] ⬇️ [Название](kp) (2025) IMDb 7.1` (без оценки) уходила в `keep` **навсегда**, а при наличии `[x]`-двойника давала дубль в каталоге. **Фикс:** сбор `watched_titles`/`watched_kp` по всем секциям кроме Новинок + удаление сирот из `keep`; нормализация **обязана срезать год и рейтинги**, иначе `проектконецсвета` ≠ `проектконецсвета2026imdb84kp84`. Флаг `_patch_dropped` добавлен в ранний выход (иначе запись файла пропускалась). `cli.py` — явные ветки вывода `👍/👎/[x]/drop`. Проверено: удаляется ровно 1 строка, 683 → 682; каталог 404 → 403. Бэкапы: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`, код — `parser.py.bak-orphan-*`, `.bak-guard-*`, `.bak-bs-*`, `cli.py.bak-cli-*`. Полный разбор — секция «ОСИРОТЕВШИЕ СТРОКИ» выше; связь с каталогом Lampa — [[personal/plans/lampac-watchlist-plugin]].
- 🔴 **[2026-09-22] ПИТФОЛЛ правки `parser.py` скриптами: двойное экранирование съедает слэши.** `r'\1'` → реальный байт `\x01`, `r'\b'` → байт `\x08` (backspace). `compile()`/`ast.parse()` проходят — синтаксис валиден, а регекспы **молча не работают** (нормализация не срезала рейтинги, сироты не находились). **Правило: после записи патча проверять файл на `chr(1)` и `chr(8)`, а не только на синтаксис.** По возможности править через `patch`/Obsidian, а не генератором строк.
- 🔴 **[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` перестанет видеть новые закачки.
+11 -1
View File
@@ -296,6 +296,17 @@ Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js
6. **Кириллица в inline-jq внутри `ssh '...'`** ломает команду (tirith + quoting).
Выносить в файл или убирать строки с кириллицей.
7. **Осиротевшие строки в `## Новинки` давали дубли в каталоге.**
Строки без `👍`/`👎` (например `- [ ] ⬇️ Название`) `process-thumbs` не трогал —
они висели в Новинках навсегда. Если фильм уже `[x]` в главном списке — получался дубль:
непросмотренный «Конец света» при просмотренном оригинале.
**Фикс в `watchlist-sync/parser.py`** (удаление сирот + флаг `_patch_dropped`) и `cli.py`.
Разбор: [[family/projects/watchlist-automation]], секция «ОСИРОТЕВШИЕ СТРОКИ».
8. **Двойное экранирование при правке `parser.py` скриптами.**
`r'\1'` превращался в байт `\x01`, `r'\b'` — в `\x08`. Синтаксис валиден, регекспы молчат.
**Проверять файл на `chr(1)` и `chr(8)` после патча.**
### Управление
```bash
@@ -324,7 +335,6 @@ ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \
```
## Правила отбора и сортировки (утверждено Alex 2026-09-22)
### Фильтрация
| Правило | Действие |