2543 lines
210 KiB
Markdown
Executable File
2543 lines
210 KiB
Markdown
Executable File
---
|
||
created: '2026-05-20'
|
||
updated: '2026-09-27'
|
||
status: active
|
||
tags:
|
||
- kraken
|
||
- watchlist
|
||
- automation
|
||
- movies
|
||
---
|
||
# Watchlist Automation — Полный Flow
|
||
|
||
Полная цепочка: как `family/documents/movies-watchlist.md` превращается в скачанный контент.
|
||
|
||
## ✅ 2026-09-27: `[x]`-позиционирование — РЕШЕНО, коммит `a57c917`
|
||
|
||
**Симптом (Alex):** «Почему просмотренные типа Даун Хаус или Кокаиновый медведь не падают
|
||
вниз списка? Да и новинки как будто давно не обновлялись».
|
||
|
||
**Спецификация** ([[family/projects/watchlist-sync]] §1.4): строка встаёт **под последней
|
||
`[ ]` своей секции**, т.е. в **НАЧАЛО блока просмотренных** этой секции. Для ВСЕХ секций
|
||
со списком (Фильмы / Мультфильмы / Аниме). НЕ «в начало файла», НЕ «в конец файла», НЕ в
|
||
отдельную секцию. Смысл слова Alex «вниз» = уход из зоны `[ ]` вниз, в блок `[x]`.
|
||
|
||
### ✅ РЕШЕНИЕ: `reflow_watched()` в `sync_up.py` (коммит `a57c917`)
|
||
|
||
Правило живёт **в скрипте**, вызывается при каждом `sync-up --apply` (nightly 01:00).
|
||
Обрабатывает и новые отметки, и уже накопленные. Идемпотентна: повторный прогон = 0 перемещений.
|
||
|
||
```python
|
||
# sync_up.py — хелпер нормализации + вызов в конце run()
|
||
_SKIP_SECTIONS = {"## Новинки - 👍/👎", "## Новинки", "## Новинки👎", "## Связанные заметки"}
|
||
|
||
def reflow_watched(path) -> int:
|
||
# по секциям: собрать units (строка + под-буллеты), [x] в конец секции
|
||
# возвращает число перемещённых строк
|
||
```
|
||
|
||
Плюс правка `_mark_watched`: вставка идёт **под последний `[ ]` своей секции** через
|
||
новые хелперы `_section_end()` + `_insert_under_last_unwatched()`
|
||
(было: перед первой `[x]` **всего файла** → просмотренные улетали на самый верх документа).
|
||
|
||
**Бэкапы кода:** `sync_up.py.bak-xdown`, `sync_up.py.bak-reflowfn`.
|
||
|
||
### ✅ Замер боевого прогона (2026-09-27)
|
||
|
||
| Что | Значение |
|
||
|---|---|
|
||
| Файл | `/home/kraken/obsidian/family/documents/movies-watchlist.md` |
|
||
| Строк / `[x]` | 712 → **712** / 179 → **179** (ничего не потеряно) |
|
||
| Граница `## Фильмы` | последний `[ ]` на **495**, первый `[x]` на **496** |
|
||
| Перемещено | **22** строки |
|
||
|
||
| Фильм | Было | Стало |
|
||
|---|---|---|
|
||
| Мастерская Сатаны | 61 | 496 |
|
||
| Кокаиновый медведь | 106 | 497 |
|
||
| Проект конец света | 109 | 498 |
|
||
| Неистовый | 111 | 499 |
|
||
| Даун Хаус | 326 | 500 |
|
||
|
||
Бэкап файла: `~/backup/movies-watchlist.md.before-reflow-20260927-221616`.
|
||
|
||
Проверка идемпотентности: второй прогон → `moved: 0`.
|
||
|
||
### 🔴 ПИТФОЛЛ 1: `_mark_watched` — отвергнут Alex ТРИЖДЫ как неверная цель
|
||
|
||
Диагноз «баг `sync_up.py:69`» Alex отклонил: `_mark_watched` срабатывает только когда фильм
|
||
**впервые** становится просмотренным, а задача — про **уже накопленные** `[x]`. Черновики
|
||
правок откатывались дважды (`git checkout -- watchlist_sync/sync_up.py`).
|
||
Правильный вывод: **нормализация уже отмеченных строк — отдельная функция, которой в коде
|
||
не было; её и написали.** Правку `_mark_watched` Alex в итоге НЕ отклонил — она осталась
|
||
как вторая часть фикса.
|
||
|
||
### 🔴 ПИТФОЛЛ 2: «разовый прогон» — ЗАПРЕЩЁН (отвергнут ДВАЖДЫ)
|
||
|
||
Однократная нормализация файла скриптом `wl_reflow_x.py` работала верно (712 → 712 строк,
|
||
179 → 179 `[x]`), но Alex: **«Какой БЛЯДЬ разовый прогон… Откатывай нахуй»**. Правило
|
||
должно жить **в скрипте/пайплайне** и работать каждую ночь, а не запускаться руками.
|
||
|
||
**Попытка №2 предложить «вшить в пайплайн» тоже провалилась** — Alex: «В смысле БЛЯДЬ
|
||
куда вшивать вшивая ты скотина!!!» → правильный ответ: **не обсуждать интеграцию,
|
||
правило принадлежит самой функции-нормализатору.** Вопрос «sync-up или process-thumbs?»
|
||
задавать НЕЛЬЗЯ. Финал: Alex → «В скрипт блядь».
|
||
|
||
### ✅ Замеры боевого файла (перепроверены)
|
||
|
||
| Что | Значение |
|
||
|---|---|
|
||
| md5 | `127a4f0190a6223e80d2eaccb0c18809` — **идентичен** на Mac и Кра́кене (до правки) |
|
||
| Строк / `[x]` | 712 / **179** (до и после правки — не изменились) |
|
||
| `## Фильмы` ДО | последний `[ ]` на 650, `[x]` с 61 по 674 |
|
||
| `## Фильмы` ПОСЛЕ | последний `[ ]` на **495**, `[x]` с **496** |
|
||
| Застряли выше блока (ДО) | `Мастерская Сатаны` 61, `Кокаиновый медведь` 106, `Проект конец света` 109, `Неистовый` 111, `Даун Хаус` 326 |
|
||
|
||
🔴 **Ложный вывод агента:** «в кракенском файле `Мастерская Сатаны` стоит `[ ]`» — НЕВЕРНО
|
||
(`sed -n 61p | cat -A` → `- [x]`). Файлы идентичны по md5. **Перед утверждением о
|
||
расхождении машин — сверять md5 обеих сторон.**
|
||
|
||
### Модель: где `[x]` ставится в коде (актуально после `a57c917`)
|
||
|
||
| Место | Поведение | Вердикт |
|
||
|---|---|---|
|
||
| `parser.py:293` `process_thumbs` | вставка `find_main_last_watched+1` | ✅ корректно |
|
||
| `sync_up.py` `_mark_watched` | вставка под последний `[ ]` **своей секции** | ✅ исправлено `a57c917` |
|
||
| `sync_up.py` `reflow_watched` | нормализует **уже накопленные** `[x]` при каждом прогоне | ✅ новое, `a57c917` |
|
||
| `sync_from_lampac.py:259` | вызывает `sync_up._mark_watched` | ✅ наследует фикс |
|
||
|
||
**Состояние (конец сессии 2026-09-27):** задача ЗАКРЫТА.
|
||
- `cf73666` — коммит ДО правки (зафиксировано чужое изменение `sync_down.py`).
|
||
- `a57c917` — правка: `reflow_watched()` + фикс `_mark_watched`.
|
||
- Боевой файл приведён к правилу, проверено 712/179 и идемпотентность.
|
||
- Порядок работ: **коммит ДО → правка → деплой → прогон.** Бэкап в план не входил.
|
||
|
||
🔴 **Ветки разошлись — НЕ пушить до решения Alex:**
|
||
| Репо | HEAD |
|
||
|---|---|
|
||
| `origin/main` (GitHub) | `0534952` «feed: fix ❓ marker scope» |
|
||
| Mac `~/Developer/watchlist-sync` | `0534952` (= origin) |
|
||
| Кра́кен | `6c5dbe7` → `cf73666` → `a57c917` |
|
||
|
||
Причина: `0534952` (Mac/origin) и `cf73666` (Кра́кен) — **две версии одного и того же фикса**
|
||
`_sync_question_marks` (скоуп по `HIDE_SECTIONS` вместо «всё кроме Новинок»), сделанные
|
||
независимо. `cf73666` был незакоммичен на Кра́кене и зафиксирован агентом.
|
||
Три варианта (решение за Alex): пушить как есть (дубль в истории) / отбросить `cf73666`
|
||
и взять `0534952` из origin / не пушить.
|
||
|
||
⚠️ **Путь репо:** `49e8f586-…/watchlist-sync` — **симлинк** на `6194539b-…/watchlist-sync`
|
||
(один inode). `.pth` editable-установки указывает на `6194539b` — это не два репозитория.
|
||
|
||
**«Новинки не обновлялись» — две разные вещи:**
|
||
1. Каталог Lampa обновляется каждую ночь: `watchlist.json` 27.09 01:07, `count: 403`
|
||
(films 357 / new 32 / cartoons 7 / anime 7).
|
||
2. `## Новинки` в `.md` — последний `discover` 21.09. 27.09 (вс) скрипт залогировал только
|
||
sync-vault pull/push, без шага `discover`. **Причина не выяснена — открытый вопрос.**
|
||
Плюс `sync_down.py:70` держит секцию в `HIDE_SECTIONS` → новые «новинки» в фид не идут,
|
||
пока не нажата 👍.
|
||
|
||
**Питфоллы jq по фиду:**
|
||
```bash
|
||
F=.../lampac/wwwroot/watchlist.json
|
||
jq -r '.channels | to_entries[] | "\(.key) (\(.value.title)): \(.value.items | length)"' $F
|
||
jq -r '.count' $F # 403
|
||
```
|
||
🔴 Канал — объект `{title, items}`, НЕ массив → `jq -c ".channels.films | .[0]"` даёт
|
||
`Cannot index object with number`.
|
||
🔴 `## Новинки` в HIDE_SECTIONS → `grep` по фиду НЕ найдёт позиции из Новинок.
|
||
Фид содержит только `Status.UNWATCHED` (`sync_down.py:326`) — ни одного `[x]`.
|
||
|
||
## ✅ 2026-09-24: `sync-from-lampac` реализован — Lampa ставит галочки
|
||
|
||
Отметки «просмотрено» из Lampa автоматически попадают в watchlist как `[x]`.
|
||
Коммит `ef44f3e` (код), план — [[personal/plans/lampac-watched-to-watchlist]] §10.
|
||
|
||
**Ночной пайплайн теперь:**
|
||
```
|
||
sync-vault (pull) → process-thumbs → resolve --recheck → [sync-down ПРОПУЩЕН]
|
||
→ sync-up (Jellyfin, non-fatal) → sync-from-lampac --apply (NEW)
|
||
→ sync-vault (push)
|
||
```
|
||
|
||
**Как работает матч** (`watchlist_sync/sync_from_lampac.py`):
|
||
```
|
||
timecodes.identity ('movie-674' / '674_movie', строгие формы)
|
||
→ tmdb_id
|
||
→ 1) .resolve_cache.json (candidate.tmdb_id → ключ = заголовок .md) — ТОЧНО
|
||
→ 2) фолбэк: watchlist.json (href → name TMDB-ru / original + год ±1)
|
||
→ _mark_watched ([x]) — ⚠️ ИСПРАВЛЕНО 2026-09-27: ФАКТИЧЕСКИ вставляется НЕ в блок
|
||
просмотренных, а «выше первой [x] в файле» — баг, см. ниже «2026-09-27»
|
||
```
|
||
|
||
**Критерий просмотренного:** `deleted=0` И `percent >= 90` (env
|
||
`LAMPAC_PERCENT_THRESHOLD`, CLI `--percent`). `watched_at` — НЕ критерий
|
||
(бывает >0 при 18%). Сериалы `tv-*` — скип, этап 2.
|
||
|
||
**Ключевые факты:**
|
||
- ✅ БД `TimeCode.sql` — **на ХОСТЕ** (bind-mount `./database:/lampac/database`,
|
||
исправлено 2026-09-24 13:38): `/srv/dev-disk-by-uuid-6194539b-…/lampac/database/TimeCode.sql`,
|
||
модуль читает напрямую, **`docker cp` больше не нужен**. ⚠️ Раньше было «только
|
||
внутри контейнера, НЕ на volume» — УСТАРЕЛО. WAL: свежие данные в `-wal`, голый
|
||
`cp` одного `.sql` = пустая оболочка; читать каталог целиком или `sqlite3 X ".backup Y"`.
|
||
`sqlite3` в контейнере нет.
|
||
- `Sync.sql` (bookmarks) и `Sisi.sql` — ПУСТЫЕ: закладки Lampa не
|
||
синхронизируются на Lampac, TimeCode — единственный канал просмотров.
|
||
- `timecodes`: UNIQUE(user, identity) — одна строка на юзера+фильм,
|
||
percent обновляется на месте.
|
||
- Ручной прогон: `uv run watchlist-sync sync-from-lampac` (dry-run по
|
||
умолчанию, `--apply` для записи).
|
||
|
||
**.env (Kraken, `$WS/.env`, бэкап `.env.bak-lampac-20260924`):**
|
||
```
|
||
LAMPAC_TIMECODE_DB=lampac:/lampac/database/TimeCode.sql
|
||
LAMPAC_WATCHLIST_JSON=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/wwwroot/watchlist.json
|
||
LAMPAC_PERCENT_THRESHOLD=90
|
||
```
|
||
|
||
**nightly:** md5 новой `edf0e34530692dbb7b6fb9644f1b45a7` (было
|
||
`4401882e...`), бэкап `~/scripts/watchlist-nightly.sh.bak-lampac-20260924`.
|
||
Полный ручной прогон 01:05–01:07 ✓, новый шаг ~2 сек, non-fatal.
|
||
|
||
**Побочный фикс:** `_normalize` в `sync_up.py` не делал `.strip()` после
|
||
удаления небуквенных — заголовки с хвостовым ⭐ молча проваливали
|
||
`_mark_watched` (в т.ч. в Jellyfin-канале). Фикс в `ef44f3e`.
|
||
|
||
**Тест:** синтетическая БД (8 сценариев: cache/json матч, 18%, tv, deleted,
|
||
уже-[x], левый id, dry-run) — 8/8 ✓; `--apply` на копии .md — diff ровно
|
||
2 ожидаемые строки. Бэкап до работы:
|
||
`~/backup-watchlist-before-lampac-sync-20260924-005630/`.
|
||
|
||
**Осталось:** калибровка порога живым досмотром (досмотреть фильм из
|
||
списка в Lampa, вычитать percent, при ≠90 поправить env); сериалы этап 2;
|
||
bind-mount для `/lampac/database` (риск потери истории при пересоздании
|
||
контейнера — отдельный апрув). Проверка в UI: после первого реального
|
||
досмотра фильм пропадает из «Мой список» в Lampa после ночного прогона
|
||
(каталог 02:30 выкидывает `[x]`).
|
||
|
||
## 🔴 2026-09-27: `[x]` НЕ падает вниз списка — баг `_mark_watched` (диагноз)
|
||
|
||
**Симптом Alex:** «Почему просмотренные в watchlist типа Даун Хаус или Кокаиновый
|
||
медведь не падают вниз списка? Да и новинки как будто давно не обновлялись»
|
||
|
||
**Корень** — `watchlist_sync/sync_up.py:69`, `_mark_watched`:
|
||
|
||
```python
|
||
insert_at = next((i for i, l in enumerate(lines) if re.match(r'\s*-\s*\[x\]', l)), None)
|
||
lines.insert(insert_at, raw)
|
||
```
|
||
|
||
`next(...)` = **первая** `[x]`-строка файла. Докстринг обещает `top of the [x] section`,
|
||
фактически получается `top of file`. Расхождение кода и комментария = баг.
|
||
|
||
**Факт из `.md` 27.09:** `- [x] Даун Хаус` — строка **326**, окружена `[ ]`-соседями.
|
||
Блок просмотренных реально: **421 → 674**. `Кокаиновый медведь` — строка 106,
|
||
`Проект конец света` — 109. Между ними и блоком ~130 строк со `[ ]`.
|
||
|
||
**Аудит:** 178 `[x]`-строк в главных секциях, из них **4** выше блока 421–674 —
|
||
все четыре пришли от `sync-from-lampac` после 25.09. Остальные 174 уже стояли
|
||
в блоке, и вставка «выше первой `[x]`» для них работала правильно.
|
||
|
||
**Готовый инструмент уже есть:** `parser.py:401 find_main_last_watched(ls)` — её
|
||
использует только `process_thumbs` (`insert_at = last_watched + 1`). `_mark_watched`
|
||
её не зовёт. Фикс — ~5 строк, **НЕ применён, ждёт команды Alex**.
|
||
|
||
**Каталог Lampa при этом НЕ затронут** — проверено: `watchlist.json` 27.09 01:07,
|
||
`count: 403` (films 357 / new 32 / cartoons 7 / anime 7), ни одного `[x]` в фиде.
|
||
Ночной пайплайн отработал полностью, бэкап `TimeCode.sql` за 27.09 в 01:09 создан.
|
||
|
||
**Второй симптом — «новинки не обновлялись»:** каталог обновляется каждую ночь,
|
||
а секция `## Новинки` в `.md` — нет: последний `discover` был **21.09**. Крон стоит
|
||
(`0 9 * * 0`), но в логе `watchlist-discover.sh` за 27.09 (воскресенье) шага
|
||
`discover` нет — только `sync-vault` pull и push, причём push на 5 минут раньше pull.
|
||
|
||
**`jq`-питфолл фида:** `.channels.films | length` → **2** (ключи `items`/`title`),
|
||
а не число позиций. Правильно: `.channels.films.items | length` → 357.
|
||
|
||
Полный разбор, аудит и план фикса — [[personal/plans/lampac-watched-to-watchlist]] §11.
|
||
|
||
## 🔴 2026-09-23: канал «Lampa → `[x]`» найден; фиксы сирот закоммичены
|
||
|
||
### Коммит `ffe7974` — закрыт должок по грязному дереву
|
||
|
||
В рабочем дереве `watchlist-sync` висели **незакоммиченные** правки, которые уже
|
||
работали боевым прогоном (ночной 02:30 использовал именно их):
|
||
|
||
| Файл | Δ | Что |
|
||
|---|---|---|
|
||
| `watchlist_sync/parser.py` | +65 | фикс «ОСИРОТЕВШИЕ СТРОКИ» в `process_thumbs()`: `watched_titles`/`watched_kp` по всем секциям кроме Новинок, `_norm_title()`, вырезание сирот из `keep`, флаг `_patch_dropped` в раннем выходе |
|
||
| `watchlist_sync/cli.py` | +7 | явные ветки вывода `👍 / 👎 / [x] / [drop]` (было обобщённое `else`) |
|
||
|
||
Закоммичены **отдельным** коммитом, не смешаны с новым кодом.
|
||
Предыдущий HEAD был `a131208` (2026-09-21).
|
||
|
||
`sync_up._mark_watched` **уже содержит** фикс эмодзи-префикса — `_PREFIX` поглощает
|
||
эмодзи с обеих сторон `⬇️`. Работает, правок не требует, переиспользуется.
|
||
|
||
### Найден реальный канал просмотров — модуль Lampac `TimeCode`
|
||
|
||
`sync_up` читает **Jellyfin**, но Jellyfin больше не источник: `BaseItems` — 53,
|
||
`movie` — **0**, `UserData.IsPlayed=true` — **0**. Смотреть стали через Lampa+TorrServer.
|
||
|
||
**Фактический источник просмотров — Lampa**, и она уже пишет их на сервер в
|
||
SQLite-БД модуля `TimeCode`:
|
||
|
||
| | |
|
||
|---|---|
|
||
| БД | `$H/lampac/database/TimeCode.sql` (таблица `timecodes`) |
|
||
| Модуль | `/lampac/module/Sync/TimeCode/` (`Controller.cs`, `plugin.js`, `README.md`) |
|
||
| Ключ | `identity` = TMDB-ID (`movie-1758655` / `1758655_movie`) |
|
||
| Прогресс | `percent` (флага «просмотрено» нет — политика клиента, см. README) |
|
||
|
||
🔴 **`watched_at` — НЕ признак досмотра.** Живая запись: `watched_at = 1790053739498`
|
||
при **`percent = 18`**. Значит любое обновление таймкода пишет `watched_at`.
|
||
Порог ставить **только по `percent`**.
|
||
|
||
План реализации синка `timecodes` → `[x]`:
|
||
[[personal/plans/lampac-watched-to-watchlist]].
|
||
|
||
### ✅ `.14` = провод, `.15` = WiFi. Оба — один Кра́кен (Alex, 2026-09-23)
|
||
|
||
🔴 **Ранее в этом доке было «`ssh kraken` резолвится в мёртвый `.15`» — ЭТО НЕВЕРНО.**
|
||
`.15` не мёртвый: это **WiFi** (`wlan0`), `.14` — **провод** (`eth0`), ходим по `.14`.
|
||
|
||
```
|
||
eth0 UP 192.168.1.14/24 metric 100 ← основной, default route
|
||
wlan0 DOWN ← отключён (см. ниже)
|
||
```
|
||
|
||
**WiFi `wlan0` ОТКЛЮЧЁН (2026-09-23).** `.15` больше не выдаётся, остался только провод `.14`.
|
||
При попытке гасить интерфейс: `ip link set wlan0 down` **не держится** — `systemd-networkd`
|
||
поднимает обратно (`Activation Policy: up` от wpa_supplicant). Рабочий путь — правка
|
||
`/etc/netplan/30-wlan0.yaml` + `netplan apply`. ⚠️ `rfkill` на Кра́кене **НЕТ**
|
||
(`command not found`).
|
||
|
||
**Настоящие причины обрывов ssh** (обе воспроизведены 2026-09-23):
|
||
1. **Перегрузка sshd** — падало при `load average: 4.69` (после ребута поднимались
|
||
контейнеры). Симптом: `Connection closed by UNKNOWN port 65535` /
|
||
`Connection timed out during banner exchange`. Лечится повтором и короткими
|
||
командами, не сменой адреса.
|
||
2. 🔴 **`sudo` через SSH без `< /dev/null` рвёт соединение.** Проверено многократно:
|
||
```bash
|
||
ssh kraken 'sudo docker restart x && sudo docker ps' # ❌ рвётся
|
||
ssh kraken 'sudo docker restart x < /dev/null && … < /dev/null' # ✅
|
||
```
|
||
Надёжнее — весь скрипт файлом: `scp` → `sudo bash /tmp/x.sh < /dev/null`.
|
||
3. **Тяжёлый `jq` в одной строке с `ssh`** (json 180 KB) — обрыв. Разбивать, тяжёлое — файлом.
|
||
|
||
**DNS-имя `kraken` — ИСПРАВЛЕНО И РАБОТАЕТ (2026-09-23).** Было: `dhcp_staticlist` нёс имя
|
||
`kraken` у **обоих** интерфейсов, плюс `dnsmasq.conf.add` имел `address=/kraken/192.168.1.15`
|
||
(жёстко на WiFi). Стало: `address=/kraken/192.168.1.14`, имя у wlan0-лиза снято.
|
||
Ребут роутера **НЕ потребовался**. Проверено: `dig +short kraken` → `.14`, ping идёт,
|
||
`ssh kraken@kraken` заходит.
|
||
|
||
🔴 **Настоящая причина «залипшего» `.15` (стоила часов — не повторять):**
|
||
|
||
| Действие | Почему НЕ помогало |
|
||
|---|---|
|
||
| Удаление лизы из `/var/lib/misc/dnsmasq.leases` | **Не тот файл** — рабочий в `/tmp/var/lib/misc/dnsmasq.leases` |
|
||
| `service restart_dnsmasq` | **Не убивает процессы** — старый ответ живёт в памяти |
|
||
| `killall -SIGHUP dnsmasq` | Не сбрасывает DNS-кэш ответов |
|
||
| `dhcp-ignore-names=lan` | Неверный синтаксис (нужен `tag:`); строку убрать |
|
||
| `dnsmasq.postconf` скрипт | Не нужен — ASUSWRT читает `conf.add` нативно; даёт дубль блока в конфиге |
|
||
|
||
**Что сработало:**
|
||
```bash
|
||
# рабочий лиз-файл — /tmp, чистить его
|
||
ssh admin@192.168.1.1 'grep -v "192.168.1.15" /tmp/var/lib/misc/dnsmasq.leases > /tmp/l.new \
|
||
&& cat /tmp/l.new > /tmp/var/lib/misc/dnsmasq.leases'
|
||
# ЖЁСТКО убить процессы (restart_dnsmasq их не убивает!)
|
||
ssh admin@192.168.1.1 'killall -9 dnsmasq; sleep 2; service restart_dnsmasq; sleep 6'
|
||
# проверить
|
||
sudo dscacheutil -flushcache; dig +short kraken @192.168.1.1 # → 192.168.1.14
|
||
```
|
||
⚠️ `reboot` роутера **из агента невозможен** — в жёстком блоклисте Hermes (`BLOCKED (hardline)`).
|
||
`pgrep`/`rfkill` на роутере и Кра́кене отсутствуют. Определить рабочий лиз-файл:
|
||
`for p in <PID1> <PID2>; do ls -l /proc/$p/fd | grep leases; done`.
|
||
Полный разбор: скилл `home-network-config` § «РЕШЕНО БЕЗ РЕБУТА».
|
||
|
||
### Состояние стека после ребута (замер 2026-09-23)
|
||
|
||
14/14 контейнеров поднялись сами. Radarr/Sonarr queue `0`, Transmission 9 торрентов
|
||
всё `Done/Idle` (Sum down `0.0`). Диск **31%** (539G/1.8T). Lampac `:9118` → 200,
|
||
Jellyfin `:8096` → 302. Ночные скрипты отработали: cleanup 02:00, nightly 01:00,
|
||
catalog 02:30–02:32 (каналов 4, позиций 392; `.md` 708 строк / 615 позиций).
|
||
|
||
✅ **DNS `kraken` → `.14` — РАБОТАЕТ** (проверено 2026-09-23, без ребута роутера).
|
||
`wlan0` DOWN, `.15` не выдаётся. Разбор граблей — в разделе про DNS ниже.
|
||
|
||
#### ✅ Проверка API стека — рабочая последовательность (2026-09-23)
|
||
|
||
Ключи читать **с диска**, не через `docker exec`:
|
||
```bash
|
||
D="/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker"
|
||
RK=$(grep -o '<ApiKey>[^<]*' $D/radarr/config/config.xml | cut -d'>' -f2)
|
||
curl -s --max-time 10 "http://localhost:7878/radarr/api/v3/queue?pageSize=50" \
|
||
-H "X-Api-Key: $RK" | jq -r '.totalRecords' # → 0
|
||
# sonarr: порт 8989, путь .../sonarr/config/config.xml
|
||
```
|
||
⚠️ `docker exec` без `< /dev/null` **рвёт SSH-сессию**. Скрипт передавать целиком:
|
||
`ssh kraken 'bash -s' < /tmp/script.sh` — устойчиво, в отличие от длинных inline-команд.
|
||
Результат 2026-09-23: Radarr queue `0`, Sonarr queue `0`.
|
||
|
||
## 🔴 2026-09-22: АВТОСКАЧКА НА HDD ВЫКЛЮЧЕНА (по требованию Alex)
|
||
|
||
### ✅ 2026-09-24: защита СНЯТА (по требованию Alex) — актуальное состояние
|
||
|
||
Логика: из watchlist в arr автоматом ничего не добавляется, защита мешала только
|
||
ручным добавлениям («для вручную добавленных закачка должна стартовать»).
|
||
|
||
| Что | Было (защита 2026-09-22) | Стало (2026-09-24) |
|
||
|---|---|---|
|
||
| `arr.py`: `monitored` + `searchForMovie` / `searchForMissingEpisodes` | `False` | **`True`** — коммит `ef7e7af` (push): добавление сразу ищет и качает. 🔴 **Вечером того же дня `arr.py` УДАЛЁН** (`a524f5d`) вместе с переходом `sync-down` на фид Lampa — см. «Консолидация» ниже |
|
||
| Radarr `rssSyncInterval` | `0` | **15 мин** (`PUT /api/v3/config/indexer`, HTTP 202) |
|
||
| Sonarr `rssSyncInterval` | `0` | **15 мин** (там же) |
|
||
| `sync-down` в кроне | вырезан | **остался вырезан** — авто-пути из watchlist нет, добавление только руками |
|
||
| `monitored` существующих (34 фильма / 13 сериалов, все без файлов) | снят | **контент удалён целиком** — см. ниже «контент удалён» |
|
||
|
||
Проверки после: очереди Radarr/Sonarr `0`, `monitored=true: 0 из 34`, auth не тронут
|
||
(`forms` / `/radarr` / 7878), RSS-задача `next − last = 900s`.
|
||
|
||
🔴 **Питфолл Radarr 6.4 / Sonarr 4.0.20:** `rssSyncInterval` живёт НЕ в `/api/v3/config/host`
|
||
(поля там физически нет), а в **`/api/v3/config/indexer`** — PUT полным объектом с `id`
|
||
(частичный молча возвращает старое). Через host-config тупик: валидатор требует непустой
|
||
`allowedHosts` при `authenticationRequired != Enabled` и не принимает звёздочку.
|
||
|
||
**✅ Консолидация РЕАЛИЗОВАНА 2026-09-24:** генератор каталога слит в `sync-down`.
|
||
Было две независимых цепочки — `lampac/scripts/gen_watchlist.py` (878 строк, свой матчер
|
||
TMDB, не знал про `resolve` и его маркеры) и `sync_down.py` (arr-путь). Теперь **один
|
||
модуль**: `sync-down` резолвит через `resolver.resolve()` и пишет фид Lampa
|
||
(`wwwroot/watchlist.json`, `watchlist-<cat>.json`, `cards/`, `cards.json`, `missing.json`).
|
||
`arr.py` **удалён** (`git rm`, коммит `a524f5d`), ⬇️ больше не ставится нигде.
|
||
|
||
Детали, три открытия при переносе (фильтры рейтинга/ужасов в фиде НЕ нужны;
|
||
❓ не исключает позицию из фида; спорные позиции берутся из `all_candidates[0]` + ❓)
|
||
и открытые пункты — [[personal/plans/lampac-watchlist-plugin]] § «ПЕРЕПИСЫВАНИЕ».
|
||
|
||
🔴 **Блокер выката (2026-09-24):** резолвер проекта матчит хуже старого генератора —
|
||
сверка по `tmdb_id` дала **86 позиций с другим фильмом**. Причина: в `gen_watchlist.py`
|
||
есть ступень «КП-по-номеру из ссылки» (`kinopoisk.ru/film/N/` → КП → TMDB), в
|
||
`resolver.py` её нет; до удаления arr-контента это маскировал Radarr-фастпатч.
|
||
На живой фид НЕ выкатывалось. Поэтому `lampac/scripts/gen_watchlist.py`,
|
||
`mark_missing.py` и `watchlist-catalog.sh` (крон 02:30) **пока НЕ удалены** —
|
||
оттуда берётся код `kp_search_by_id` / `_docs_to_tmdb`.
|
||
Метрики dry-run: 268 (с фильтрами) → 332 (без фильтров) → **397** (с `all_candidates[0]`).
|
||
|
||
### ✅ 2026-09-24 (вечер): контент удалён — Radarr/Sonarr пустые
|
||
|
||
Alex: «если они ни в чем не участвуют, удали все фильмы/сериалы». Проверка перед удалением:
|
||
каталог Lampa (`gen_watchlist.py`) использовал Radarr только как фаст-патч постеров/tmdbId
|
||
— **9 карточек из 392** (2%), остальные 362 через TMDB API (основной путь, кэш 434 записи),
|
||
сериалов в каталоге нет вообще, import-lists пустые, файлов у контента ноль.
|
||
|
||
**Удалено:** 34/34 фильма (Radarr) + 13/13 сериалов (Sonarr), `deleteFiles=false`,
|
||
`addImportExclusion=false`, ноль ошибок. Бэкапы: `radarr.db.bak-20260924-163600`,
|
||
`sonarr.db.bak-20260924-163600` (в config-каталогах; онлайн `.backup`, рестарта не было).
|
||
Очереди после: 0/0.
|
||
|
||
**Регрессионный тест** (прогон генератора с пустым Radarr в `/tmp`, живой каталог не трогал):
|
||
|
||
| Метрика | утро (с Radarr) | после удаления |
|
||
|---|---|---|
|
||
| карточек | 392 | **392** |
|
||
| без TMDB (→❓) | 30 | **30** |
|
||
| постеров | 388 | **390** |
|
||
| источники | Radarr 9, TMDB 362 | Radarr 0, TMDB 371 |
|
||
|
||
Ноль потерь. **3 карточки сменили матч** (в `.md` нет года, Radarr и TMDB матчили вслепую):
|
||
- `Unbroken` 2003 → **Несломленный 2014** (вероятно правильнее)
|
||
- `Shogun` 1968 → **Убийца сёгуна 1980**
|
||
- `Олдбой` 2003 → **Олдбой 2013** (⚠️ ремейк Спайка Ли; если имелся в виду оригинал
|
||
Пака Чхан-ука — добавить год в строку 134 `.md`: `Олдбой 2003`)
|
||
|
||
Фаст-патч Radarr в генераторе оставлен — при пустом Radarr молча не срабатывает,
|
||
оживёт сам при ручных добавлениях.
|
||
|
||
### ✅ 2026-09-24 (вечер): отдельный генератор Lampa ВЫПИЛЕН — фид пишет sync-down
|
||
|
||
**Требование Alex:** «не отдельный скрипт на лампу, а один sync скрипт и писал сразу
|
||
lampa feed». Раньше связка была двумя механизмами: `sync-down` (отдельный шаг) +
|
||
`lampac/scripts/gen_watchlist.py` (878 строк) по своему крону в 02:30.
|
||
|
||
**Стало:** `watchlist_sync/sync_down.py` собирает фид сам, в ночном пайплайне на
|
||
своём прежнем месте (01:00), генератор и его обвязка удалены:
|
||
|
||
| Было | Стало |
|
||
|---|---|
|
||
| `sync-down` → Radarr/Sonarr (`arr.py`) | `sync-down` → `wwwroot/watchlist.json` + `cards/` + `missing.json` |
|
||
| `lampac/scripts/gen_watchlist.py` (878) + `mark_missing.py` (179) | `.bak-removed-20260924-181942` |
|
||
| `~/scripts/watchlist-catalog.sh` + крон `30 2 * * *` | убран (`.bak-removed-…`), крон почищен |
|
||
| `arr.py` (Radarr/Sonarr API) | удалён из репы (в истории git) |
|
||
| маркер `⬇️` | больше не ставится — закачки нет, маркер ничего не значит |
|
||
|
||
Коммиты: `a524f5d` (перенос), `471e9df` (ступень КП + тайбрейк), `0534952` (скоуп ❓).
|
||
|
||
**Резолв фида — 2 ступени** (`sync_down.run`):
|
||
1. **КП-по-номеру из ссылки `.md`** (`resolver.resolve_by_kp_id`) — машинный ключ,
|
||
не зависит от переименований. Без этой ступени на названиях расходилось ~86 позиций.
|
||
2. **Матч по названию** (`resolver.resolve`). Неоднозначные → берётся кандидат с
|
||
лучшим `score`, ничьи разбивает рейтинг (Alex 2026-09-24) + ❓ в `.md`.
|
||
🔴 Только рейтинг использовать НЕЛЬЗЯ: выбрал бы «Семь Самураев» (★8.5) вместо
|
||
«Семь» (1995, ★8.4) и «Злая наука» вместо нужного фильма.
|
||
|
||
**Питфоллы, найденные по ходу:**
|
||
- 🔴 Фильтры рейтинга/ужасов в фиде — НЕДОПУСТИМЫ: это правила закачки Radarr,
|
||
а не каталога. С ними терялось ~120 позиций (37 ужасов). Каталог отдаёт всё.
|
||
- 🔴 `resolve_by_kp_id` не заменяет поиск: у 66 строк с ❓ ссылки на Кинопоиск
|
||
в `.md` НЕТ, они идут в слепой матч по названию. Куда показывать — уточнять год.
|
||
- 🔴 `resolve` с годом-хинтом не фильтровал КП-кандидатов: «Злая 2024» → `Cattiva`
|
||
(1991) со score 1.0. Исправлено — год отсекает КП-документы.
|
||
- 🔴 `_sync_question_marks` считал скрытым всё, кроме секции «Новинки» → ❓ не
|
||
попадал в Фильмы/Мультфильмы/Аниме. Скрывать можно только `HIDE_SECTIONS`.
|
||
- `WatchItem` нельзя класть в выходной JSON (не сериализуется) — только словари.
|
||
|
||
**Проверено на живом:** фид 390 позиций (4 канала), lampac HTTP 200, фид 200,
|
||
карточка 200, ночной прогон целиком зелёный, vault запушен.
|
||
Бэкап фида до правки: `/tmp/feed-before-*.json`; ночного скрипта —
|
||
`watchlist-nightly.sh.bak-feed-20260924-181926`.
|
||
|
||
🔴 **Рестарт lampac после обновления фида НЕ НУЖЕН** (уточнено 2026-09-24, вопрос Alex).
|
||
`wwwroot/` — bind-mount, скрипт пишет файл **с хоста**, внутри контейнера он виден
|
||
сразу (проверено: один и тот же inode `119669729` и размер на хосте и в `lampac`).
|
||
Алекс: «обновление фида же не потребует передёрга?» — верно, не требует.
|
||
`docker restart lampac` был унаследован от старого `catalog.sh`, который так сбрасывал
|
||
`hybridCache` каталога; цена — приложение падает на ~минуту (компиляция модулей,
|
||
HTTP `000`). Из ночного скрипта рестарт убран. Если каталог в Lampa залип
|
||
(кэш-ключ `catalog:{plugin}:{search}:{sort}:{cat}:{page}` живёт в памяти процесса) —
|
||
обновить страницу в Lampa или переключить категорию, а не ронять контейнер.
|
||
|
||
Дальше — история и cookbook той защиты.
|
||
|
||
**Контекст.** Задача — Lampa на ТВ показывает каталог из `movies-watchlist.md` и играет
|
||
через TorrServer. Alex: «мы перестаем качать торренты на диск!» → автоскачка Radarr/Sonarr
|
||
выключена фактически. Модуль Catalog/Lampa — в [[personal/plans/lampac-watchlist-plugin]].
|
||
|
||
### Выполнено (замер до → после)
|
||
|
||
| Сервис | Очередь | `monitored` | `rssSyncInterval` |
|
||
|---|---|---|---|
|
||
| **Radarr** (:7878) | 6 → **0** | 45 → **0** | 30 → **0** |
|
||
| **Sonarr** (:8989) | 19 → **0** | 13 → **0** | 15 → **0** |
|
||
|
||
Бэкапы: `radarr.db` / `radarr.db.bak-20260922-123404`, `sonarr.db` / `.bak-20260922-123404`
|
||
(+ оба `config.xml`) в `/srv/...6194539b.../docker/{radarr,sonarr}/config/`.
|
||
|
||
~~**Метаданные НЕ удалены** — это важно: 50 фильмов остались в Radarr с `tmdbId` (все 50),
|
||
постерами (46) и рейтингом. Они нужны каталогу Lampa.~~ 24 файла на диске (Jellyfin) не тронуты.
|
||
🔴 Устарело 2026-09-24: контент удалён полностью; каталог Lampa перешёл на TMDB-фолбэк
|
||
без потерь (392 карточки, см. выше «контент удалён»). Опасение «нужны каталогу» не
|
||
подтвердилось — Radarr обслуживал 2% карточек как кэш.
|
||
|
||
**Проверка пассивности:** Transmission за 45 с — NET `290GB/26.7GB`, BLOCK `214GB/393kB`
|
||
**не сдвинулись**. Логи Radarr (`grep -iE "search|download|grab"`) за 2 мин — пусто.
|
||
|
||
### ✅ 2026-09-22 (вечер): `sync-down` вырезан из крона — дыра закрыта СНАРУЖИ
|
||
|
||
Второй слой защиты после ручного снятия `monitored`. Даже если `arr.py` не тронут,
|
||
**автоматического пути в закачку больше нет вообще**:
|
||
|
||
`watchlist-nightly.sh` вызывал `sync-down --apply --batch 2`. Вызов **убран**.
|
||
Крон теперь: `sync-vault → process-thumbs → resolve --recheck → sync-up → sync-vault`.
|
||
|
||
Бэкап: `/home/kraken/scripts/watchlist-nightly.sh.bak-20260922-182655`.
|
||
Сумма новой: `4401882e3b1307a73b3330a70b59ecfe`.
|
||
|
||
🔴 **Порядок защиты** (действовал 2026-09-22 → 2026-09-24; частично снята — см. секцию выше):
|
||
1. Ручное снятие `monitored` + `rssSyncInterval: 0` — **отменено 2026-09-24** (rss=15;
|
||
`monitored` существующих оставлен снятым).
|
||
2. **`sync-down` вне крона** — остаётся и после 2026-09-24 (авто-добавлений из watchlist нет).
|
||
|
||
`sync_down.py` как код оставлен — ручной запуск возможен.
|
||
Проверка отсутствия автозакачки:
|
||
```bash
|
||
ssh kraken "grep -ln 'radarr_add\|sonarr_add\|radarr_search\|sync-down --apply' \
|
||
/home/kraken/scripts/*.sh"
|
||
```
|
||
Должны быть только комментарии. Детали — [[family/projects/watchlist-sync]].
|
||
|
||
### ✅ 2026-09-23: пост-ребут проверка стека — все чисто
|
||
|
||
После перезагрузки Кра́кена (uptime 23 мин на момент проверки) — **все 14 контейнеров
|
||
поднялись сами**, ручного вмешательства не потребовалось.
|
||
|
||
| Проверка | Значение |
|
||
|---|---|
|
||
| Radarr queue | `totalRecords: 0` |
|
||
| Sonarr queue | `totalRecords: 0` |
|
||
| Transmission | 9 торрентов, все `100% Done / Idle`, Sum down `0.0` |
|
||
| Lampac `:9118` | HTTP 200 |
|
||
| Jellyfin `:8096` | HTTP 302 |
|
||
| Диск `/dev/sda1` | 539G / 1.8T = **31%** |
|
||
|
||
**Ночные скрипты отработали** (все `2026-09-23`): `cleanup` 02:00 (27.9% → ниже порога,
|
||
no-op), `nightly` 01:00, `catalog` 02:30→02:32 (каналов 4, позиций 392), простановка ❓
|
||
02:32 (708 строк, 615 позиций, `.bak-questions`). Фид `watchlist.json` 106 867 Б и
|
||
`movies-watchlist.md` 30 479 Б — оба mtime `02:32`. Vault git чистый, последний коммит
|
||
2026-09-23 14:47.
|
||
|
||
### 🔴 2026-09-23: `[x]` из Lampa НЕ проставляется — канал найден, но не подключён
|
||
|
||
**Вопрос Alex:** «А у нас из лампы уходят отметки просмотрено в список галочками как было в jellyfin?»
|
||
**Ответ: НЕТ.** Ни Lampa, ни Jellyfin сейчас не дают `[x]`. Задача в работе — см.
|
||
[[personal/plans/lampac-watched-to-watchlist]].
|
||
|
||
**Почему Jellyfin-канал мёртв (замеры 2026-09-23):**
|
||
|
||
| Проверка Jellyfin (`BaseItems` / `UserData`) | Значение |
|
||
|---|---|
|
||
| `BaseItems` всего | 53 |
|
||
| из них `movie` | **0** |
|
||
| из них `series` | 1 (плейлист/коллекция) |
|
||
| `UserData.IsPlayed=true` | **0** |
|
||
| `PlaybackActivity` | **0** |
|
||
|
||
Причина: смотрят через **Lampa + TorrServer**, а не Jellyfin. Плюс с 2026-09-22
|
||
автозакачки выключены → новые файлы в Jellyfin не приходят. `sync_up.py`
|
||
(`GET /Users/{id}/Items?IsPlayed=true` → `[x]`) читает пустоту.
|
||
|
||
**🔑 Где реально живёт история просмотров — модуль Lampac `TimeCode`:**
|
||
|
||
| Элемент | Значение |
|
||
|---|---|
|
||
| Модуль | `/lampac/module/Sync/TimeCode/` (`Controller.cs`, `plugin.js`, `SqlContext.cs`) |
|
||
| БД | `/lampac/database/TimeCode.sql` (SQLite). **На хосте:** `$H/lampac/database/TimeCode.sql` |
|
||
| Таблица | `timecodes` |
|
||
| Колонки | `Id, user, identity, card, item, position, duration, percent, profile, deleted, watched_at, updated_at, extra` |
|
||
| Пишет | Lampa → `POST /timecode/add` (легаси) / `/timecode/set` (нативный), пачками ≤10 req/s |
|
||
| Ключ личности | `identity = {tmdb_id}_movie` — готовый ключ для матча со списком по TMDB-ID |
|
||
|
||
Живая запись на 2026-09-23 (единственная в БД):
|
||
```
|
||
identity = 1758655_movie card = movie-1758655
|
||
position = 1222.0 duration = 6898.0 percent = 18.0
|
||
watched_at = 0 deleted = 0
|
||
```
|
||
|
||
🔴 **Питфолл: `sqlite3` в контейнере `lampac` НЕТ** (`sh: sqlite3: not found`).
|
||
Читать БД так: `docker cp lampac:/lampac/database/TimeCode.sql /tmp/` + на хосте
|
||
`python3 -c "import sqlite3; ..."`. На хосте Кра́кена `python3` есть.
|
||
⚠️ `viewed.json` и `config.db` в `/lampac/data/ts/` — это **TorrServer'овские**, к
|
||
TS-истории просмотров Lampa отношения не имеют (не путать).
|
||
|
||
**Решение (план, ждёт апрува):** новый модуль `sync_from_lampac.py` в `watchlist-sync` —
|
||
читает `timecodes` (`deleted=0` и (`percent>=90` или `watched_at>0`)) → матч по `tmdb_id`
|
||
из `watchlist.json` (у **346/346** films `tmdb_id` есть) → `[x]` через существующий
|
||
`sync_up._mark_watched` → вызов в `watchlist-nightly.sh` после `sync-up`.
|
||
Полный план, грабли и чек-лист приёмки — [[personal/plans/lampac-watched-to-watchlist]].
|
||
|
||
### 🔜 2026-09-22: УДАЛЕНИЕ авто-закачек — инвентаризация снята, ЖДЁТ КОМАНДЫ
|
||
|
||
Alex: *«закачки отменять и проверить еще раз что заново не добавятся»* →
|
||
*«удалить все добавленные автоматикой закачки в т.ч. файлы. и radarr и sonarr»*.
|
||
|
||
**Сделано:** полный дамп + разбор, что именно авто-добавлено. **Удаление НЕ
|
||
запущено** — Alex не подтвердил список (16 позиций vs все 50).
|
||
|
||
```
|
||
дамп (единственный откат, корзины у Radarr нет):
|
||
/tmp/radarr-sonarr-dump-20260922-184351/radarr-movies.json
|
||
/tmp/radarr-sonarr-dump-20260922-184351/sonarr-series.json
|
||
```
|
||
|
||
**Критерий «авто»: `added >= 2026-09-20`** (окно работы `sync-down`; пайплайн
|
||
watchlist'а включён в сентябре). Всё добавленное 18.05–04.06 в Radarr — ручное.
|
||
|
||
| Система | Всего | Авто-добавлено | Комментарий |
|
||
|---|---|---|---|
|
||
| Radarr | 50 фильмов | **16** (12 с файлами, 4 без) | added 20–21.09 |
|
||
| Sonarr | 13 сериалов | **0** | все 18.05–01.06, авто нет |
|
||
|
||
Sonarr-список (для протокола, НЕ удалять): `Our Flag Means Death`, `Stranger
|
||
Things`, `The Cyberfarm`, `Magical Police Department`, `Место встречи изменить
|
||
нельзя`, `MOZU`, `Delicious in Dungeon`, `Rick and Morty`, `Severance`,
|
||
`Brassic`, `Double or Quits`, `Futurama`, `True Detective`.
|
||
|
||
🔴 Полная таблица 16 Radarr-позиций с id и `hasFile` — в
|
||
[[personal/tech/lampac-deployment#📋 Инвентаризация авто-закачек в Radarr/Sonarr (2026-09-22)]].
|
||
Там же — команда удаления и питфолл `deleteFiles=true`
|
||
(**Radarr по умолчанию НЕ удаляет файл с диска**).
|
||
|
||
**Гарантия «заново не добавятся» — проверена фактом (два независимых барьера):**
|
||
```
|
||
1. sync-down вне крона → автозапуска нет
|
||
2. все 50 фильмов Radarr: monitored=false → поиск раздач не стартует
|
||
+ очередь (queue) 0 записей
|
||
+ wanted/missing 0 записей
|
||
```
|
||
|
||
⚠️ **НЕ сделано:** коммит правки `watchlist-nightly.sh` в git (вырезанный
|
||
`sync-down` живёт только на Кра́кене, в репо — старая версия).
|
||
|
||
### Правильные вызовы API (проверено)
|
||
|
||
```bash
|
||
# 1. отмена загрузки — файлы на диске НЕ трогаются, blocklist НЕ трогать
|
||
DELETE /api/v3/queue/<id>?removeFromClient=true&blocklist=false
|
||
|
||
# 2. снятие monitored — КЛЮЧ ПОЛЯ РАЗНЫЙ у Radarr и Sonarr!
|
||
PUT /api/v3/movie/editor {"movieIds": [...], "monitored": false} # Radarr
|
||
PUT /api/v3/series/editor {"seriesIds": [...], "monitored": false} # Sonarr, НЕ "ids"
|
||
|
||
# 3. RSS off — отправлять ВЕСЬ объект конфига с id, иначе молча вернёт старое
|
||
PUT /api/v3/config/indexer {"rssSyncInterval": 0, "id": 1, ...остальное}
|
||
```
|
||
|
||
Ключи API: Radarr `cbcb8ec320104359920c0f611d503c7b`, Sonarr `15fbec32f1cb4b05b1c2b229ce27359b`
|
||
(из `<ApiKey>` в `config.xml`).
|
||
|
||
### 🔴 Питфоллы этой операции
|
||
|
||
1. **`urllib` НЕ идёт по 307, а `curl -sL` — идёт.** Первый прогон: все 25 DELETE вернули
|
||
`HTTP Error 307`, **ничего не удалилось**. Штатный `urllib` на 307/308 превращает
|
||
DELETE в GET и теряет тело. Фикс — свой `HTTPRedirectHandler`, сохраняющий метод и тело
|
||
(код в [[personal/tech/lampac-deployment]], раздел Radarr).
|
||
2. **Sonarr группирует эпизоды сезона под одним `id`** — после удаления первой записи
|
||
остальные дают `404 Not Found`. Это **не сбой**, они ушли вместе с родительской.
|
||
3. **`PUT config/indexer` требует весь объект с `id`** — частичный `{"rssSyncInterval":0}`
|
||
молча возвращает прежнее значение (Radarr понадобился второй заход).
|
||
4. **Кириллица в inline-`jq` внутри `ssh '...'`** снова сломала команду
|
||
(`unexpected INVALID_CHARACTER`). Ключи объекта — латиницей.
|
||
|
||
### Откат
|
||
|
||
```bash
|
||
curl -sL -X PUT ".../api/v3/movie/editor" -d '{"movieIds":[...],"monitored":true}'
|
||
curl -sL -X PUT ".../api/v3/series/editor" -d '{"seriesIds":[...],"monitored":true}'
|
||
curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1,...}'
|
||
cp radarr.db.bak-20260922-123404 radarr.db && docker restart radarr
|
||
```
|
||
|
||
## Полная цепочка
|
||
|
||
```
|
||
[вс 09:00] watchlist-discover ← KP API топ фильмов 2024-2025
|
||
↓ добавляет в секцию "Новинки 👍/👎"
|
||
movies-watchlist.md
|
||
Alex ставит 👍 / 👎
|
||
↓
|
||
[ежедн 09:00] watchlist-resolve ← SSH Кракен → resolve → commit
|
||
[ежедн 10:00] watchlist-sync-down
|
||
→ process-thumbs (перемещает 👍 → начало списка)
|
||
→ sync-down (добавляет батч в Radarr/Sonarr)
|
||
↓
|
||
Radarr (7878) / Sonarr (8989)
|
||
→ Prowlarr (9696) → RuTracker / Kinozal(M) / NoNaMe / etc.
|
||
→ Transmission (9091)
|
||
→ /media/movies-radarr или /media/series-sonarr
|
||
↓
|
||
router.py (Custom Script) → читает жанры из NFO/TMDb
|
||
→ symlink в /media/{movies,cartoons,documentaries,...}/
|
||
↓
|
||
Jellyfin (8096) → rescan → виден контент
|
||
```
|
||
|
||
## Статусы в movies-watchlist.md
|
||
|
||
| Символ | Значение | Действие |
|
||
|--------|---------|---------|
|
||
| `[ ]` | не смотрел, в очереди | sync-down добавляет в Radarr/Sonarr |
|
||
| `[x]` | просмотрено | sync-up из Jellyfin watched history |
|
||
| `⬇️` | добавлено в Radarr/Sonarr | ждёт скачки |
|
||
| `👍` | нравится (новинка) | process-thumbs переносит в начало → попадёт в sync-down |
|
||
| `👎` | не хочу (новинка) | process-thumbs перемещает в `## Новинки👎` |
|
||
| `🗓️` | upcoming (ещё не вышел) | discover помечает, не скачивается |
|
||
| `❓` | не найдено / ambiguous | ждёт ручного уточнения |
|
||
| `🧟♀️` | 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]` (просмотренные) с `⭐` игнорируются. Работает **из всех секций** (`## Фильмы`, `## Новинки`, `## Мультфильмы`, `## Аниме`).
|
||
|
||
## Правила отбора в `sync-down` (актуально с 2026-09-21)
|
||
|
||
Приоритет определяется **двумя факторами**, а не позицией строки:
|
||
|
||
| Условие | Поведение |
|
||
|---------|-----------|
|
||
| `⭐` (U+2B50) в строке фильма | **первыми**, фильтры рейтинга И ужасов **не применяются** |
|
||
| ужас (`is_horror()`, genre_id=27) без `⭐` | **отсеивается**, с логом `[🧟] … horror, skipped` ⬅️ новое 2026-09-21 |
|
||
| все остальные `[ ]` | **по порядку файла** (сверху вниз), рейтинг ≥ 6.0 |
|
||
| рейтинг < 6.0 (включая `0.0`) | отсеивается, с логом `[−] … skipped` |
|
||
| `[x]` | пропускается |
|
||
| `❓` / `🗓️` (ambiguous) | пропускается, обрабатывается отдельно |
|
||
|
||
⚠️ **Стоп-кран по месту (2026-09-21):** перед любым добавлением проверяется `MIN_FREE_GB = 100`.
|
||
Если свободно меньше — прогон останавливается (`[STOP] free space … — nothing queued`).
|
||
Проверка через **`shutil.disk_usage`**, не `df`. Именно отсутствие этой проверки заполнило диск
|
||
на 100% 2026-09-21 — см. [[family/plans/watchlist-disk-overflow-fix]].
|
||
|
||
**Фильтр ужасов работает по ЖАНРУ (`candidate.is_horror()`, TMDB genre_id=27), а не по эмодзи `🧟♀️`.**
|
||
В dry-run 2026-09-21: **33 horror item(s) skipped**, 14 rating skipped.
|
||
Результат для Alex: 41 фильм с `🧟♀️` (многие — свежие релизы 2025–2026) больше не качается.
|
||
Обход — поставить `⭐` в строку фильма.
|
||
|
||
⚠️ **Порядок файла ≠ рейтинг.** До 2026-09-20 код сортировал по рейтингу TMDB и игнорировал позицию в списке — из-за этого в батч попадали фильмы «не из начала списка». Исправлено коммитом `ada47da`.
|
||
|
||
⚠️ **`--batch` снижен 5 → 2** (2026-09-21) после инцидента с диском. Меняется в
|
||
`~/scripts/watchlist-nightly.sh` (root:root — править через `sudo cp`).
|
||
|
||
⚠️ **`⭐` = обход фильтров, а не смена сортировки.** Это принципиальная позиция Alex («Я блядь сказал в приоритете завела»): звёздочка означает явное «хочу», поэтому рейтинг для неё не проверяется вовсе.
|
||
|
||
## Структура секций movies-watchlist.md
|
||
|
||
```
|
||
## Новинки - 👍/👎 ← discover добавляет сюда
|
||
## Фильмы ← основной список, отступ 2 пробела ( - [ ])
|
||
## Мультфильмы
|
||
## Аниме
|
||
## Новинки👎 ← без пробела перед 👎
|
||
## Связанные заметки
|
||
```
|
||
|
||
**Важно:** `process_thumbs` автоматически определяет отступ из `## Фильмы` и сохраняет при переносе. `SECTION_THUMBDN = "## Новинки👎"` (без пробела) — должно точно совпадать с заголовком.
|
||
|
||
## ✅ Крон pipeline ВОССТАНОВЛЕН (2026-09-20 21:21)
|
||
|
||
**Задача закрыта.** Pipeline был мёртв с 2026-06-20 (проверено аудитом ниже), теперь расписание вернули.
|
||
|
||
Установлено в crontab юзера `kraken`:
|
||
```
|
||
# Watchlist: process-thumbs + resolve + sync-down + sync-up (ежедневно 01:00)
|
||
0 1 * * * bash /home/kraken/scripts/watchlist-nightly.sh >> /tmp/watchlist-nightly.log 2>&1
|
||
|
||
# Watchlist: discover — новинки из KP API (воскресенье 09:00)
|
||
0 9 * * 0 bash /home/kraken/scripts/watchlist-discover.sh >> /tmp/watchlist-discover.log 2>&1
|
||
```
|
||
|
||
**Как ставилось** (идемпотентный скрипт, бэкап перед установкой):
|
||
```bash
|
||
crontab -l > ~/crontab-backup-$(date +%Y%m%d-%H%M%S).txt # бэкап
|
||
crontab /tmp/crontab-new.txt # установка
|
||
crontab -l # проверка
|
||
```
|
||
Бэкап: `~/crontab-backup-20260920-212155.txt`
|
||
|
||
**Проверено после установки:**
|
||
- `systemctl is-active cron` → `active`
|
||
- `grep -c watchlist /var/spool/cron/crontabs/kraken` → **2** (обе строки подхвачены демоном)
|
||
- Логи пишутся в `/tmp/` — **теряются при перезагрузке**. Перенести в постоянное место, если нужно сохранять историю.
|
||
|
||
⚠️ **В `docker exec hermes-kraken hermes cron list` по-прежнему «No scheduled jobs»** — это НЕ про watchlist. Watchlist-скрипты детерминированные, живут в системном cron, в Hermes-планировщике их не должно быть. Не искать их там.
|
||
|
||
## 🔴 2026-09-22: ОСИРОТЕВШИЕ СТРОКИ в `## Новинки` — баг и фикс
|
||
|
||
**Симптом (сообщил Alex):** «Проект Конец света» показывался в каталоге Lampa как непросмотренный,
|
||
хотя в главном списке он `[x]` (просмотрен) и был скачан. Alex предположил «опять синк не работает».
|
||
|
||
**Корень — НЕ синк, а `process_thumbs()` в `parser.py`.**
|
||
|
||
Регексп функции матчит только строки с плиткой:
|
||
```python
|
||
_THUMBS_RE = r'^(\s*-\s*\[.\]\s*)(👍|👎)?\s*(.*?)\s*(👍|👎)?$'
|
||
```
|
||
Ветвление (актуальное поведение):
|
||
- `👍` → строка **копируется** в начало `## Фильмы`, из `## Новинки` **удаляется**
|
||
- `👎` → переносится в `## Новинки👎`
|
||
- `[x]` → переносится в конец `[x]`-блока главного списка
|
||
- **всё остальное (в т.ч. `⬇️` без плитки) → попадает в `keep` = остаётся в `## Новинки` НАВСЕГДА**
|
||
|
||
**Осиротевшая строка в файле (была):**
|
||
```
|
||
- [ ] ⬇️ [Проект «Конец света»](kinopoisk/1382256/) (2026) IMDb 8.4 KP 8.4
|
||
```
|
||
`⬇️` = «добавлен в Radarr», но плитки нет → ни под одно правило не подпадала → вечно `[ ]`
|
||
в `## Новинки` → вечно в каталоге Lampa как непросмотренная.
|
||
|
||
**Почему не нашёл поиском по названию:** написания разные —
|
||
`Проект «Конец света»` (Новинки, со ссылкой KP) vs `Проект конец света` (главный список, `[x]`, без ссылки).
|
||
|
||
### Фикс 1 — `parser.py::process_thumbs()`: удаление сирот
|
||
|
||
Добавлен блок: **собирает ключи всех `[x]`-строк из ВСЕХ секций** (кроме `## Новинки` и `## Новинки👎`),
|
||
затем выкидывает из `keep` строки, чей ключ совпал.
|
||
|
||
Сравнение по **двум** ключам: ссылка Кинопоиска (`kinopoisk/NNN/`) **или** нормализованное название.
|
||
|
||
```python
|
||
# Нормализация: срезаем эмодзи, год, рейтинги, регистр, пробелы, «ё»→«е»
|
||
_norm_title = lambda s: re.sub(r'[^а-яa-z0-9]+', '',
|
||
re.sub(r'\b(?:IMDb|KP)\s*[\d.,]+', '', re.sub(r'\(?\b(19|20)\d{2}\b\)?', '', s.lower()))).replace('ё','е')
|
||
```
|
||
|
||
Результат: `Проект «Конец света» (2026) IMDb 8.4 KP 8.4` → `проектконецсвета`,
|
||
`Проект конец света` → `проектконецсвета`. Совпадают.
|
||
|
||
⚠️ **Критично — снятие раннего `return []`.** Блок стоит ПОСЛЕ сбора `keep`, но в функции есть
|
||
проверка `if not thumbs_up and not thumbs_down and not watched_items: return []`.
|
||
Когда патч удаляет последнюю сироту, все три списка пусты → функция выходила **до записи файла**,
|
||
и удаление терялось. Условие расширено флагом `orphans_removed`, чтобы запись состоялась.
|
||
|
||
### Фикс 2 — `cli.py`: корректный вывод действия
|
||
|
||
`[drop]` попадал в `else`-ветку и печатался как перенос в «Новинки 👎» (косметика, но путает логи).
|
||
Разведены явные ветки по типу действия.
|
||
|
||
### Проверка
|
||
Прогон на **копии** боевого файла: удаляется **ровно 1** строка, 683 → 682, потерь нет.
|
||
После прогона на боевом: каталог Lampa **404 → 403 позиции**.
|
||
`## Новинки`: 34 → 33. «Конец света» отсутствует во всех 4 категориях.
|
||
|
||
Бэкап перед правкой: `~/backup-watchlist-before-orphan-fix-20260922-113925.md`
|
||
|
||
### 🩹 ГЛАВНЫЕ ГРАБЛИ: patch-скрипты съедают обратные слэши (наступил 3 раза)
|
||
|
||
При правке через write_file+python **двойное экранирование превращает escape-последовательности
|
||
в сырые байты**. Синтаксис остаётся валидным, `ast.parse()` проходит, но **регекспы молча не работают**.
|
||
|
||
| Написано | Осело в файл | Эффект |
|
||
|---|---|---|
|
||
| `r'\1'` | байт `\x01` | подстановка ссылки ломается |
|
||
| `r'\b'` | байт `\x08` (backspace) | `\b` не матчит границу слова → рейтинги `IMDb 8.4 KP 8.4` не срезаются |
|
||
|
||
**Как диагностировать:** ключ `проектконецсветаimdb84kp84` — рейтинги остались в ключе, значит `\b` мёртв.
|
||
Проверять регекспы побайтово (`repr()` строки из файла), а не глазами через `read_file`.
|
||
|
||
**Правило:** для regex-замен в этих скриптах использовать `chr(92)+'1'` / `chr(92)+'b'` либо
|
||
проверять `repr()` после записи. Отлаживать через печать `repr()` реально загруженной строки.
|
||
|
||
### 📌 Следствие для будущего
|
||
Ночной cron (01:00, `watchlist-nightly`) теперь **сам вычищает** такие сироты при каждом прогоне.
|
||
Если в каталоге снова появится просмотренный фильм — первым делом `grep` по `## Новинки`.
|
||
|
||
## 🔴 Инцидент переполнения диска (2026-09-21) — историческая справка
|
||
|
||
**Симптом:** диск `/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 отдавал
|
||
`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 питфоллы
|