Files
obsidian-vault/personal/plans/lampac-watchlist-plugin.md
T

3288 lines
210 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
created: '2026-09-22'
namespace: personal
related:
- '[[personal/plans/lampac-kraken]]'
- '[[personal/tech/lampac-deployment]]'
- '[[family/projects/watchlist-sync]]'
- '[[family/projects/watchlist-automation]]'
- '[[family/documents/movies-watchlist]]'
- '[[personal/plans/lampac-watched-to-watchlist]]'
status: fifth-fix-2026-09-25-global-inject-flag-deployed-awaiting-verdict
tags:
- lampac
- lampa
- watchlist
- catalog
- webos
- plugin
- kraken
- radarr
- sonarr
- torrserver
- tmdb
- card-parse
- feed
- matcher-parity
- partnext
- latency
- wlonmain
- wlprefetch
- activity-hook
title: Watchlist в UI Lampa — каталог из списка
type: plan
updated: '2026-09-25'
---
# Watchlist в UI Lampa — каталог из списка
## 🔍 2026-09-25: РАССЛЕДОВАНИЕ — фид виден на всех экранах + «долго грузится»
## 🟡 2026-09-25 (продолжение): ДУБЛИ СЕКЦИЙ — «watchlist снова уплыл вниз»
**Симптом Alex:** «Ну чето нифига не поменялось. Главная - фильмы - главная -
watchlist снова уплыл вниз».
**Диагноз по DOM-замеру — не «уплыл», а ДУБЛЬ.** В `document.querySelectorAll('.items-line')`
оказалось **13 заголовков вместо 9**:
```
0:Новинки 1:Фильмы 2:Мультфильмы 3:Аниме ← наши (вверху, верно)
4:Сейчас смотрят ... 8:В высоком качестве ← родные CUB
9:Новинки 10:Фильмы 11:Мультфильмы 12:Аниме ← НАШИ ВТОРОЙ РАЗ (внизу)
```
Визуально нижний комплект читается как «секции уплыли вниз», но это **второй экземпляр**:
первый остался наверху, Lampa его не удаляет.
### 🔴 КОРНЕВАЯ ПРИЧИНА: флаг вставки велся на МАССИВЕ, а не глобально
Было: `if (... && !parts.__wl_done) { parts.__wl_done = true; wlInject(...) }`.
🔴 **`parts` приходит НОВЫМ массивом на каждый вызов `partNext`** — на нём нет пометки
от прошлого вызова, поэтому `!parts.__wl_done` **истинно всегда**, и `wlInject`
добавлял второй комплект через `unshift`. Флаг, повешенный на объект пула, не работает.
Плюс `window.__wl_done = false` в хуке сбрасывал **не ту переменную**: проба показала
`wl_done = None` (undefined в глобальной области) — хук не влиял ни на что.
### ✅ ФИКС: глобальный `window.__wl_injected`
```js
// wlPatchPartNext:
if (wlOnMain() && Lampa.Arrays.isArray(parts) && !window.__wl_injected) {
window.__wl_injected = true; // ГЛОБАЛЬНО, не на parts
wlInject(parts, menu);
}
// wlWatchActivity: при уходе с главной разрешаем вставку при возврате
if (comp !== 'main') { window.__wl_injected = false; return; }
```
Логика: ушли с главной → флаг снят; вернулись → вставили ОДИН раз; повторные вызовы
`partNext` на той же главной → флаг держит, дублей нет.
**Инвариантов в сборщике — 20.** Добавлены: `глобальный флаг вставки` (=2, синхронная
и async-ветки), `флаг ставится` (=2), `флаг снимается при уходе` (=1),
`НЕТ старого parts.__wl_done` (=0).
🔴 **Проверка, что правка реально на ТВ:** проба `window.__wl_activity_hook === true`
(уникальный маркер этой сборки). `ready`/`patch` для этого НЕ годятся — они истинны
и для старой версии, из-за чего три круга правок выглядели «не применёнными».
## 🔍 2026-09-25: РАССЛЕДОВАНИЕ — фид виден на всех экранах + «долго грузится»
**Жалоба Alex (дословно):** «есть проблема с watchlist фидом в лампе: он отображается
не только на "главной" но даже когда я выбираю категорию например "документальные"
в разделе Фильмы, то есть независимо от активного экрана. и он как будто долго
прогружается относительно основного CUB списка, почему он не моментально проявляется,
он же статичный».
**Статус: ✅ ПРИЁМКА ПРОЙДЕНА 2026-09-25 — фильтр работает. Фид снят со всех экранов
кроме главной. Осталось: предзагрузка `__wl_menu` (латентность) + 66 с на карточке.
См. «✅ ИТОГ 2026-09-25» в конце раздела.**
### ✅ РЕШЕНИЕ: `wlOnMain()` — белый список по активному activity
Alex: «В смысле всё кроме? **Только главный экран!**» — предикат сделан строгим
(`=== 'main'`), не «всё кроме списка исключений». Любое незнакомое имя activity
обязано по умолчанию секции НЕ получать.
⚠️ **ПЕРВАЯ ВЕРСИЯ (ниже) НЕРАБОЧАЯ — оставлена как история ошибки.** Она читала
`a.activity.url`; правильный уровень — `a.url` (измерено на ТВ, см. «РЕГРЕСС И ВТОРОЙ
ФИКС»). Актуальный текст — `wlUrl()` + `wlOnMain()` в разделе регресса.
```js
// ❌ НЕРАБОЧАЯ ПЕРВАЯ ВЕРСИЯ — оставлена как история ошибки, НЕ КОПИРОВАТЬ
function wlOnMain() {
try {
var a = (window.Lampa && Lampa.Activity && Lampa.Activity.active)
? Lampa.Activity.active() : null;
var act = (a && a.activity && a.activity.url) ? a.activity.url : '';
if (act === 'main') return true;
console.log('[watchlist] не главная (' + (act || '?') + ') — секции пропущены');
return false;
} catch (e) {
console.log('[watchlist] wlOnMain error', e);
return false;
}
}
```
Гард в перехватчике (`plugin-cub.js:193`):
```js
if (wlOnMain() && Lampa.Arrays.isArray(parts) && !parts.__wl_done) { ... wlInject(parts, menu); }
```
🔴 **Второе, неочевидное изменение:** `parts.__wl_done = true` теперь ставится ТОЛЬКО
внутри ветки фактической вставки. Раньше флаг мог выставиться до готовности
`window.__wl_menu` — и тогда вставка была бы закрыта НАВСЕГДА, главная осталась бы без
секций после возврата из категории.
**Файлы:**
- `/Users/admin/lampac-tmp/plugin-cub.js` — рабочая сборка (бэкап
`plugin-cub.js.bak-before-filter-<ts>`)
- `/Users/admin/lampac-tmp/build_plugin.py` — тот же текст в константе `INJECT`
(сборщик детерминированный; `plugin-cub.js` руками не правим) + **2 новых инварианта**:
`wlOnMain def` и `wlOnMain guard` — сборка падает, если фильтр пропал
**Проверки перед заливкой (все пройдены):**
```
python3 build_plugin.py → 10/10 инвариантов, 17241 байт
node --check plugin-cub.js → SYNTAX OK
```
**Деплой (выполнен):**
1. бэкап боевого — `catalog/plugin.js.bak-before-mainfilter-20260925-220809` (17476 Б)
2. `scp plugin-cub.js kraken:...6194539b.../lampac/catalog/plugin.js` → 19576 Б,
md5 `53ccdc306cf6a14f343eaeb67a8a29c3`
3. bind-mount проверен: `docker exec lampac grep -c wlOnMain
/lampac/module/Catalog/plugin.js` → **3**
4. по HTTP тоже отдаётся: `curl http://127.0.0.1:9118/catalog.js` → 41050 Б, `wlOnMain` ×3
**Осталось — перезапуск Lampa на ТВ** (webOS кэширует `catalog.js`, `docker restart
lampac` его НЕ сбрасывает):
```bash
scp /Users/admin/lampac-tmp/tv_restart.sh root@192.168.1.70:/tmp/ \
&& ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
```
**Приёмка = видно в UI** (не «API отвечает»): главная — 4 секции Watchlist сверху;
Фильмы → документальные — секций НЕТ. Если фид вылезет ещё где-то — в логе ТВ будет
строка `[watchlist] не главная (<имя>)`, по ней добавить имя в разбор.
**Что НЕ трогалось:** `window.__wl_menu`, `wlGet`/`wlAbs`/`wlHost`, `unshift`,
`watchlist.yaml`, `sync_down.py`, nightly-пайплайн.
### Причина 1 (диагноз) — в `plugin-cub.js` НЕ БЫЛО фильтра по экрану
Файл: `/Users/admin/lampac-tmp/plugin-cub.js` (сборка `build_plugin.py`;
это тот же файл, что залит в Lampac как `module/Catalog/plugin.js`).
`wlPatchPartNext()` — перехват глобального `Lampa.Api.partNext` — до фикса выглядел так:
```js
Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
try {
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
var menu = window.__wl_menu;
if (menu && Object.keys(menu).length) {
parts.__wl_done = true;
wlInject(parts, menu); // ← БЕЗУСЛОВНАЯ вставка
}
```
**Ни одного обращения к активному экрану** — это не регресс, а исходное отсутствие
проверки. Механика: `partNext` — глобальный API, через него строят страницу ВСЕ
источники. `plugin.js:30` (`main()`) и `plugin.js:36` (`category()`) **оба** вызывают
`Lampa.Api.partNext(parts_data, parts_limit, ...)`. Отсюда:
- CUB рисует главную → перехват срабатывает → наши 4 секции сверху;
- вход в категорию («документальные» внутри Фильмов) → Lampa перестраивает страницу
через **тот же** `partNext` → перехват срабатывает снова → фид снова сверху.
`wlInject()` кладёт секции через `unshift` в **начало** пула, поэтому они рисуются
поверх содержимого любой категории — ровно то, на что жаловался Alex.
### Причина 2 — «долго грузится»: замеры (не догадки)
Всё замерено `curl` на Кра́кене по `127.0.0.1:9118` (порт только изнутри хоста):
| Запрос | Время | `size_download` |
|---|---|---|
| `GET /catalog/list?plugin=watchlist&cat=films` — повторный | **4 мс** | 107 640 |
| тот же — **первый (холодный кэш)** | **2 250 мс** | 107 640 |
| `GET /watchlist-films.json` — чистая статика мимо Catalog | **1 мс** | 88 156 |
| `GET /catalog/list?plugin=watchlist&cat=new` | 2–147 мс | 8 512 |
| **`GET /catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie`** | **66 118 мс** | 939 |
🔴 **ПИТФОЛЛ ЗАМЕРА: `curl -w '%{size_download}'` показывает размер СЖАТОГО тела.**
Я сначала прочитал 107 640 как «фид всего 105 КБ, грузиться нечему» — это ошибка.
Реальные размеры: `watchlist-films.json` = **88 156 несжатых** при 347 позициях,
`cards.json` = **548 219** (данные `ls -la` на Кра́кене). Для замеров объёма брать
`ls -la` или `%{size_download}` вместе с `Accept-Encoding: identity`.
**Что из этого следует:**
1. **4 мс на прогретом кэше** — данные отдаются мгновенно, «статичный» — правда.
2. **2.25 с на ПЕРВОМ холодном запросе категории** — это парсинг JSON парсерами
модуля Catalog (`content.nodes`, `args`) при пустом `hybridCache`. Дальше — кэш.
Отсюда ощущение «не моментально проявляется»: категория, которую давно не открывали,
платит 2+ секунды, а родной CUB-список тем временем уже отрисован.
3. **66 секунд на карточке фильма** — отдельный, более серьёзный баг. 66 с на файл
939 байт из локального каталога — это не диск. `card_parse.initUrl` исполняется как
C#, `node: card`, читается `wwwroot/cards/card-movie-NNN.json`. Внутри
`CardController` что-то ждёт (таймаут внешнего вызова / `reactionsGet` к cub).
**Не диагностировано до конца — следующий шаг.**
4. **Наши секции стартуют позже родных:** `partNext` собирает пул, `parts_limit = 6`,
наши секции идут через `unshift` и грузятся отдельным HTTP-запросом на каждую
(`/catalog/list?plugin=watchlist&cat=<code>`) — 4 запроса. Родной фид CUB приходит
одним запросом, поэтому «проявляется» раньше.
### Контракт Lampac (проверено, НЕ трогать без причины)
`catalog/sites/watchlist.yaml` на Кра́кене
(`/srv/dev-disk-by-uuid-6194539b-.../lampac/catalog/sites/watchlist.yaml`):
- `plugin: watchlist` (🔴 без него ключ = `displayname` = «Мой список» → `ModInit.goInit`
вырезает кириллицу регуляркой `[^a-z0-9\-]` → site="" → HTTP 400 → «ЗДЕСЬ ПУСТО»).
- `host: http://127.0.0.1:9118`, категории `Новинки/Films/Мультфильмы/Аниме`.
- 🔴 **Первая категория в списке ИГНОРИРУЕТ `format`** и берёт `list.uri` напрямую —
поэтому первой стоит «Новинки» (`list.uri: watchlist-new.json`).
- `card_parse.initUrl` конвертирует `uri` `movie/424781` → `cards/card-movie-424781.json`,
`node: card`. `card_args.tmdb_id` обязан быть **числом** (через `format: long.TryParse`),
иначе карточка = «здесь пусто».
**Nightly-пайплайн** (`/home/kraken/scripts/watchlist-nightly.sh`, крон `0 1 * * *`,
пользователь `kraken` НА КРА́КЕНЕ): `sync-vault pull` → `process-thumbs --apply` →
`resolve --recheck --apply` → **`sync-down --apply`** (сборка фида, `LAMPAC_ROOT=`UUID-путь)
→ `sync-up --apply` → backup `TimeCode.sql` (`sqlite3 .backup`) → `sync-from-lampac --apply`
→ `sync-vault push`. Рестарта lampac в скрипте НЕТ и он не нужен (`wwwroot/` bind-mount,
тот же inode) — но при залипшем каталоге в UI помогает обновление страницы / переключение
категории. На 2026-09-25 `watchlist-catalog.sh` уже убран в `.bak-removed-20260924-181942`,
отдельной строки крона `30 2 * * *` в crontab **нет** (п.2 из списка незакрытого — закрыт).
### 🔴 РЕГРЕСС И ВТОРОЙ ФИКС 2026-09-25 (после первой заливки)
**Alex после перезапуска Lampa:** «Нету теперь списка на главной. Смотри логи.»
Секции исчезли ПОЛНОСТЬЮ — первая версия фильтра оказалась нерабочей.
**Логи сняты с живой Lampa через CDP** (ТВ `192.168.1.70:9998`, скрипт
`/Users/admin/lampac-tmp/wl_console.py` + `/tmp/wl_probe*.py`; `websocket-client`,
`Runtime.enable` + `Log.enable`):
```
act = undefined ← фильтр сравнивал с 'main', получал пустое
comp = [object Object]
wl_menu_type = undefined ← window.__wl_menu НЕ создан
catalog_all = NOMANIFEST ← Lampa.Manifest.catalogs вообще пуст
patch = true ← перехватчик стоял, фильтр работал
src = cub
```
**🔴 КОРНЕВАЯ ПРИЧИНА №1 — `url` лежит НА ВЕРХНЕМ уровне activity, а не во вложенном.**
Измерено на живом ТВ (`/tmp/wl_probe5.py`, вывод дословно):
```
Lampa.Activity.active() → {url, title, component, source, page, params, activity}
Lampa.Activity.active().activity → {component, object, slide, body, is_stopped, is_started}
↑ url ЗДЕСЬ НЕТ
```
Первая версия читала `active().activity.url` → **всегда `undefined`** → `=== 'main'`
всегда `false` → секции пропускались на главной ВСЕГДА. Фильтр не «слишком строгий» —
он смотрел не туда. **Проверять форму объекта ЗАМЕРОМ, а не по памяти: в доках
сессии 7 форма не была зафиксирована.**
**🔴 КОРНЕВАЯ ПРИЧИНА №2 — `Manifest.catalogs` пуст** → `loadCatalogs()` не получил
`/catalog` от Lampac → `window.__wl_menu` не создан → **вставлять нечего**, даже если
фильтр пропускает. Строгая первая версия заодно отсекала ЕДИНСТВЕННЫЙ вызов `wlMenu()`,
который дозагружает меню повторным запросом. То есть фильтр не только не пропускал —
он снял путь восстановления.
**Фикс (залит 2026-09-25 22:14):** новая функция `wlUrl(a)` читает **оба уровня**,
верхний первым, вложенный — фолбэк; плюс смягчающая ветка «пустой `url` + пустой
`catalogs` = инициализация главной → пропустить»:
```js
function wlUrl(a) {
if (!a) return '';
if (a.url) return a.url; // ← ПРАВИЛЬНЫЙ уровень
if (a.activity && a.activity.url) return a.activity.url; // фолбэк
return '';
}
function wlOnMain() {
var a = (window.Lampa && Lampa.Activity && Lampa.Activity.active)
? Lampa.Activity.active() : null;
var act = wlUrl(a);
if (act === 'main') return true;
if (!act) { // главная ещё инициализируется
var c = (window.Lampa && Lampa.Manifest && Lampa.Manifest.catalogs) || {};
if (!Object.keys(c).length) return true;
}
console.log('[watchlist] не главная (' + (act || '?') + ') — секции пропущены');
return false;
}
```
Ложная вставка на чужом экране при этом невозможна: без `__wl_menu` вставлять нечего.
**Сборка и деплой (все инварианты пройдены):**
```
python3 build_plugin.py → 12/12, 18281 байт
node --check plugin-cub.js → SYNTAX OK
```
Новые инварианты сборщика: `wlUrl def`, `wlUrl верхний уровень`
(`if (a.url) return a.url;`) — сборка падает, если чтение уровня откатится назад.
Бэкап: `catalog/plugin.js.bak-before-fixurl-<ts>`. Залито → 20934 Б,
`docker exec lampac grep -n "function wlUrl" -A4 /lampac/module/Catalog/plugin.js` → на месте.
**⚠️ ПЕРВЫЙ ЗАМЕР ПОСЛЕ ЗАЛИВКИ — приёмка на нём НЕ пройдена** (оставлено как
история): `url = ""` на ОБОИХ уровнях, `hasCats = false`, `menu = 0`. Alex на это
ответил: «Ты заебал. Ты блядь задачу решаешь или мозг мне ебешь?! Я жду от тебя:
готово, проверяй.» — **правильный ответ на такой сигнал: доделать и доложить,
не задавать вопросов.** Ниже выяснилось, что пустой `url` был замером в момент
загрузки, а не поломкой; приёмка пройдена после полной инициализации Lampa.
**Следующий шаг был:** проверить боевой запрос Lampa → Lampac `/catalog`. ✅ Выполнено —
`/catalog` жив с обеих сторон: с Кра́кена `http=200 size=21470 t=0.44s` (холодный),
по LAN с Mac `http=200 size=21470 t=0.01s`, тело содержит корректный
`watchlist.menu` = `{Новинки, Фильмы, Мультфильмы, Аниме}`. **Бэкенд был исправен —
проблема была только в гонке замера.**
### ✅ ИТОГ 2026-09-25: ПРИЁМКА ПРОЙДЕНА
**Alex:** «Так. Есть. Но сильно после первых специй.» — секции на главной **появились**,
фильтр работает. Осталась претензия к задержке появления.
#### Доказательство работы фильтра (замер на живой Lampa после перезапуска)
```
ready = true
patch = true ← перехватчик установлен
act_url = "movie" ← в момент замера экран = Фильмы (не главная)
menu = Новинки,Фильмы,Мультфильмы,Аниме ← __wl_menu заполнен, всё есть
dom_titles = Сейчас смотрят | Клиника | Мыс страха | Мейр из Исттауна | ...
↑ Watchlist-секций в DOM НЕТ
```
🔴 **Ключевой вывод:** на экране `movie` (Фильмы) наших секций в DOM **нет**, при том что
`menu` заполнен и `patch` активен. Это и есть доказательство, что фильтр режет по экрану,
а не «случайно ничего не нашёл»: вставлять было чем, но `wlOnMain()` вернул `false`.
#### 🔴 Почему «сильно после первых специй» — АРХИТЕКТУРНАЯ, не сетевая
Разобрано по коду `loadCatalogs()` + `wlPatchPartNext`:
1. `loadCatalogs()` идёт по ветке `else` и делает `network.silent('/catalog', ...)` —
**только после ответа** пишет `Manifest.catalogs` и `window.__wl_menu`.
То есть меню приходит **асинхронно**.
2. Главная CUB рисуется **своим** `cub.main()`, **не дожидаясь** этого запроса. Родные
секции («Сейчас смотрят», «Рекомендуем посмотреть») стартуют сразу.
3. Первый вызов `partNext` застаёт `__wl_menu` пустым → уходит в ветку `wlMenu()` —
**асинхронный** запрос `/catalog`. Пока он летит, родные секции уже отрисованы.
4. Когда `/catalog` вернулся, `wlInject` делает `unshift`, но `partNext` уже отработал
срез `slice(0, parts_limit)` (`parts_limit = 6`). Наши секции попадают в пул только
на **следующем** раунде, который добирает `data.length >= 3`.
**Отсюда отставание:** наши 4 секции — это отдельный сетевой запрос на каждую
(`/catalog/list?plugin=watchlist&cat=<code>` = 4 запроса), стартующие позже родных,
плюс попадание в пул не первым раундом. Родной фид CUB приходит одним запросом.
**План ускорения (согласован Alex: «Коммить и переходим к предзагрузке»):**
подставить `menu` из `/catalog` **синхронно**, ДО первого `partNext` — в `startPlugin()`,
параллельно с `loadCatalogs()`. Тогда к моменту отрисовки главной `__wl_menu` уже в
памяти, `wlInject` срабатывает в первом же раунде, секции встают в голову без отставания.
#### ✅ РЕШЕНО: репозиторий `~/Developer/lampac-scripts` — история НЕ в `lampac-tmp`
**Изначальная ошибка вывода (сохранена как урок).** Проверка «в лоб» дала:
```
/Users/admin/lampac-tmp → fatal: not a git repository
/srv/.../lampac (на Кра́кене) → fatal: not a git repository
```
Отсюда был сделан **неверный вывод** «коммитить некуда». На деле репозиторий
**существовал давно** — я искал его не там, потому что в доке пути к плагину были
записаны как `/Users/admin/lampac-tmp/`.
🔴 **Настоящее место: `~/Developer/lampac-scripts`** (Gitea
`git_admin/lampac-scripts`), там же `scripts/`, `tests/`. Alex дважды указал на это
(«Проект на маке в developer это в доке записано. Ведь так?!», «И на gitea залит»).
**Урок: прежде чем объявить «некуда коммитить» — искать репозиторий по
`~/Developer/`, а не только рядом с файлом.**
**Репы на Кра́кене:** только `docker/`, `obsidian/`, `watchlist-sync/`. В `lampac/`
git нет и не заводится — бэкапы там `.bak-*` (закрыты `.gitignore`).
| Артефакт | Путь |
|---|---|
| **Исходник (источник истины)** | `~/Developer/lampac-scripts/scripts/build_plugin.py` |
| **Собранный выход** | `~/Developer/lampac-scripts/scripts/plugin-cub.js` |
| Скрэтч, вне git | `/Users/admin/lampac-tmp/` |
| Боевой на Кра́кене | `.../lampac/catalog/plugin.js` + `.bak-*` рядом |
`build_plugin.py` держит весь `INJECT` в себе → файл воспроизводится
**детерминированно**, с 20 проверками инвариантов.
#### ✅ ЗАКОММИЧЕНО 2026-09-25 — репозиторий найден, «некуда коммитить» СНЯТО
Alex уточнил: «Проект на маке в developer это в доке записано. Ведь так?!» — и был прав.
🔴 **Живое место исходников плагина — `~/Developer/lampac-scripts/` (ПОД GIT), Gitea
`git_admin/lampac-scripts`.** Ранее в доке везде значился `/Users/admin/lampac-tmp/` —
это скрэтч-папка сессии 2026-09-22, **вне git** (50+ одноразовых пробников).
Именно поэтому первый заход дал «коммитить некуда»: искал в `lampac-tmp` и в `lampac/`
на Кра́кене, а репа — `lampac-scripts`. Правило из дока («скрипт, который зовёт крон на
боевой машине, обязан быть в репозитории») действовало и на плагин.
```bash
cd ~/Developer/lampac-scripts
cp .../build_plugin.py scripts/build_plugin.py
cp .../plugin-cub.js scripts/plugin-cub.js
git add scripts/build_plugin.py scripts/plugin-cub.js
git commit -m "watchlist plugin: секции CUB только на главной экране"
git push origin main
```
**Коммит `5e55a47`** — «watchlist plugin: секции CUB только на главной экране»,
2 files changed, 526 insertions(+). Запушено: `53e5803..5e55a47 main -> main` →
`https://git.mallexxx.duckdns.org/git_admin/lampac-scripts.git`.
Репы на Кра́кене (проверено `find`): только `docker/`, `obsidian/`, `watchlist-sync/`.
В `lampac/` git НЕТ — боевой `plugin.js` версионируется только через репу
`lampac-scripts` (файл кладётся в неё и оттуда заливается `scp`).
⚠️ Бэкапы боевого файла (`plugin.js.bak-*`) закрыты `.gitignore` репы (`*.bak*`) —
в git не попадают, остаются локальной страховкой рядом с файлом.
#### ⚠️ Замер «плагин не загрузился» — это была гонка, а не поломка
Сразу после `luna-send`-перезапуска проба дала:
```
ready = false plugin = false patch = false
act_url = ERR: Cannot read property 'url' of undefined
cats = NOMANIFEST
```
Выглядело как «наш плагин вообще не доехал» (`window.plugin_catalog` ставит сам
`startPlugin()`). **Фактически это был замер раньше, чем Lampa дошла до главной** —
через несколько секунд те же пробы дали `ready=true, plugin=true, patch=true`.
Также цель CDP на этот период имела `title=""` и `url=about:blank` — Lampa грузилась.
🔴 **ПРАВИЛО:** после перезапуска Lampa на ТВ **не верить первому замеру**. Либо
опрашивать `ready`, пока не станет `true`, либо искать CDP-цель по `url`-подстроке
`com.lampac.tv`, а не по `title == "Lampac"` (в момент загрузки `title` пустой,
и скрипт молча не находит цель). Alex на паузу без проверки отреагировал резко:
«Чё нахуй за слип 20?!» — **не спать, а опрашивать состояние.**
---
### Инструменты диагностики ТВ, написанные в этой сессии (переиспользовать)
| Скрипт | Что делает |
|---|---|
| `/Users/admin/lampac-tmp/wl_console.py` | CDP-клиент: сбор `console`-логов Lampa за 12 с + снимок состояния (`act`, `patch`, `menu`, `pending`, `src`) |
| `/tmp/wl_probe2..5.py` | точечные пробы структуры `Activity.active()` (ключи, типы, значения) |
🔴 **Питфоллы CDP-проб, найденные на живом ТВ:**
- `JSON.stringify(Lampa.Activity.active())` → **`TypeError: Converting circular structure
to JSON`**. Возвращать поля по одному или собирать строку вручную.
- Обращаться к отсутствующему полю в `Runtime.evaluate` **без** обёртки — CDP отвечает
без `result.value`, и выглядит это как «все пробы пустые» (первый прогон дал девять
`<none>` подряд). Каждую пробу оборачивать в `String(...)` / `typeof ...` и проверять
`subtype == "error"`.
- `Log.enable` + `Runtime.enable` дают **0 строк** лога, если Lampa уже загрузилась —
консольные сообщения, напечатанные до подключения, не отдаются. Логи читать СРАЗУ
после перезапуска приложения, либо подтверждать состояние пробами, а не логами.
### Открытые шаги (для следующей сессии)
1. ✅ **ПРИЁМКА ПРОЙДЕНА 2026-09-25** — фильтр `wlOnMain()` работает, фид виден только
на главной. Доказательство: на экране `movie` секций в DOM нет при заполненном `menu`.
2. 🔄 **ПРЕДЗАГРУЗКА `__wl_menu`** — ВЫПОЛНЕНА, но НЕ ЗАКРЫТА: см. «ТРЕТИЙ ФИКС»
ниже. Требуется ещё один заход (фильтр по `component` + вставка при возврате).
3. **Диагностировать 66 с на карточке** — `docker logs lampac` + трейс `CardController`.
Запрос: `GET /catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie`
→ 66 118 мс, 939 Б.
4. **Matcher-parity** (главный блокер с 2026-09-24, см. ниже) — всё ещё не закрыт.
5. ✅ **Версионирование плагина — РЕШЕНО:** `lampac-scripts/scripts/`, коммит `5e55a47`.
Развилка «`lampac_plugin/` в `watchlist-sync` vs бэкапы» **закрыта**: плагин
принадлежит `lampac-scripts` (там же `gen_watchlist.py`, `deploy_lampac.py`).
---
## 🔴🔴 2026-09-25 (финал): ТРЕТИЙ ФИКС — фильтр по `component` + `wlPrefetch()`
### Приёмка Alex частичная: два новых симптома
Alex после заливки `wlPrefetch`:
> «Так, хорошо, Сейчас при загрузке приложения сработало, коммить.
> Но когда я с главной ушел в раздел фильмы там снова были watchlist разделы
> А когда вернулся на главную опять разделы watchlist не сверху»
То есть: **при холодном старте — правильно** (секции сверху, фильтр держит), но
навигация ломает обе части: уход в «Фильмы» → секции появились; возврат на главную →
секции не сверху.
### 🔴 ГЛАВНАЯ НАХОДКА: на главной `url` ПУСТОЙ, а `component === 'main'`
Замер на живой Lampa (CDP) в момент, когда открыта именно главная
(`?component=main&source=cub&page=1`):
```
active_full = url= | title=Главная - CUB | component=main | source=cub | page=1 | params=obj | activity=obj
act_url_direct = "" ← url ПУСТОЙ, а не 'main'
act_act_url = undefined ← вложенный activity.url тоже пуст
menu = Новинки,Фильмы,Мультфильмы,Аниме
cats = NOMANIFEST
```
🔴 **Вывод: проверка `url === 'main'` НЕ РАБОТАЕТ ВООБЩЕ.** Она не срабатывала ни разу —
её проходила только смягчающая ветка `if (!act) { if (!catalogs) return true; }`.
Как только `Manifest.catalogs` заполнится, **фильтр начал бы резать саму главную**.
Это была мина, а не рабочее условие.
**Правильное поле — `component`:** главная `→ 'main'`, категория `→ 'category_full'` /
`'category'`. Оно стабильно на обоих экранах, в отличие от `url`.
```js
function wlComp(a) {
if (!a) return '';
if (a.component) return String(a.component);
if (a.activity && a.activity.component) return String(a.activity.component);
return '';
}
// в wlOnMain(): if (comp === 'main') return true;
// смягчающая ветка: if (!comp) { пустые catalogs → true }
```
### Замер, доказавший что секции ВСЁ-ТАКИ вставлялись
```
cat_header = Сейчас смотрят | Рекомендуем посмотреть | Последнее добавление | Огонь! |
В высоком качестве | Новинки | Фильмы | Мультфильмы | Аниме | Фэнтези | ...
↑↑↑↑↑↑↑↑ наши 4 секции — ЕСТЬ, но ПОСЛЕ родных
wl_done = false
```
То есть на главной секции присутствовали, но стояли **после** «В высоком качестве» —
ровно тот симптом «сильно после первых секций», который Alex и называл.
### `wlPrefetch()` — предзагрузка меню при старте
```js
function wlPrefetch() {
if (window.__wl_prefetched) return;
window.__wl_prefetched = true;
wlMenu(function (m) { console.log('[watchlist] меню предзагружено: ' + Object.keys(m || {}).join(',')); });
}
// в startPlugin(), рядом с wlPatchPartNext():
// if(window.appready){wlPatchPartNext();wlPrefetch();} else { … 'ready' → wlPatchPartNext();wlPrefetch(); }
```
`/catalog` тянется **параллельно** `loadCatalogs()`, чтобы `__wl_menu` лежал в памяти
к моменту первой отрисовки главной. Меряно: `menu` заполнен при
`Manifest.catalogs = NOMANIFEST` → **на `loadCatalogs()` полагаться нельзя**, нужен
независимый запрос.
### Состояние сборки и деплоя
```
python3 build_plugin.py → 14/14 инвариантов, 18947 байт
node --check plugin-cub.js → SYNTAX OK
```
**Инварианты (14):** `INJECT=1`, `partNext def=1`, `wlInject def=1`,
`unshift (не push)=1`, `push в INJECT=0`, `startPlugin=1`,
`нет network.silent в INJECT=0`, **`wlOnMain def=1`**, **`wlComp def=1`**,
**`wlComp читает component=1`**, **`wlOnMain guard=1`**, **`wlPrefetch def=1`**,
**`wlPrefetch call=3`**. Инварианты `wlUrl def` / `wlUrl верхний уровень` — **убраны**
вместе с `wlUrl()` (поле оказалось не тем).
**Деплой выполнен:** бэкап `catalog/plugin.js.bak-before-comp-<ts>` →
`scp` (18947 Б) → проверка в контейнере
`docker exec lampac grep -c "wlPrefetch\|wlComp" /lampac/module/Catalog/plugin.js` → **4**
→ рестарт Lampa на ТВ через `luna-send` (`--- закрываю --- --- запускаю --- --- готово ---`).
### ✅ ОБА СИМПТОМА РАЗОБРАНЫ ЗАМЕРОМ — ЧЕТВЁРТЫЙ ФИКС (см. раздел ниже)
Гипотезы из третьего фикса **опровергнуты замером**. Переход
`main → category_full → main` даёт **ПУСТОЙ трейс `partNext`** — он не вызывается
на этих переходах вообще, Lampa переиспользует отрисованный пул. Фильтр по
`component` при этом **работает** (`main` → пропуск, `category_full` → рез).
Полный разбор, код фикса и статус деплоя — в разделе
«🔴 РЕШЕНИЕ НАЙДЕНО ЗАМЕРОМ: хук на `activity` (ЧЕТВЁРТЫЙ ФИКС 2026-09-25)».
**Метод замера, который это дал** (`/tmp/wl_nav.py`): обернуть `Lampa.Api.partNext`
своим трейсом (`window.__wl_trace.push(component+'|url='+url+'|n='+parts.length)`),
затем дёрнуть `Lampa.Router.call('category_full',{url:'movie',...})`, снять снапшот
`Activity.active()`, вернуться, снять трейс. **Трейс пустой = `partNext` не зовётся** —
это и был ответ.
🔴 **Питфолл метода:** `Lampa.Router.call('main')` как «вернуться на главную»
**не работает** (`ERR: Cannot read property 'url' of undefined`). Возврат на главную
через Router не эмулируется — навигацию по экранам Alex проверяет пультом.
**Правило:** `git commit` через `ssh`/длинную команду может уйти в таймаут апрува —
подтверждение Alex обязательно, повторять команду автоматически нельзя.
**Правило диагностики этой сессии:** не читать `%{size_download}` как «размер фида»
(это gzip) — брать `ls -la` или `Accept-Encoding: identity`. И — `ifconfig` на Кра́кене
отсутствует: каждая ssh-команда печатает `bash: line 1: ifconfig: command not found`
безвредным шумом, это НЕ ошибка команды.
**🔴 Питфолл инструментов этой сессии:** `execute_code` падал на `import`
(`No module named 'psutil'`) — расследование и правки сделаны через `terminal` +
`read_file` + `patch`, БЕЗ python-скриптов. `scp` файла `wwwroot/catalog.js` не нашёл:
такого пути НЕТ, `plugin.js` живёт в `lampac/catalog/plugin.js` и монтируется в
`/lampac/module/Catalog/plugin.js`. Alex отдельно и дважды требовал результат
расследования (не план, не вопрос): «задача блядь поставлена», «хули ты творишь. ты
блядь не можешь в код проекта посмотреть и в доки и все выяснить?» — **сначала читать
код проекта и доки, потом отвечать.**
---
## 🔴 ЧЕТВЁРТЫЙ ФИКС 2026-09-25: хук на `activity` (решение найдено замером)
**Симптомы Alex после третьего фикса:**
> «Так, хорошо, Сейчас при загрузке приложения сработало, коммить.
> Но когда я с главной ушел в раздел фильмы там снова были watchlist разделы
> А когда вернулся на главную опять разделы watchlist не сверху»
При холодном старте — правильно; навигация ломала обе части.
### Замер (CDP, живая Lampa): трейс `partNext` + значения полей
Скрипт `/tmp/wl_nav.py`: оборачивает `Lampa.Api.partNext` своим трейсом, затем
`Lampa.Router.call('category_full',{url:'movie',...})`, снапшот `Activity.active()`,
возврат, чтение трейса.
```
ЩАС(главная?) | component=main url= title=Главная - CUB
-> category_full {url:'movie'}
ПОСЛЕ Фильмов | component=category_full url=movie title=Фильмы
ПОСЛЕ возврата| component=category_full url=movie
ТРЕЙС: [] ← partNext НЕ ВЫЗЫВАЛСЯ НИ РАЗУ
```
| Экран | `component` | `url` |
|---|---|---|
| Главная | **`main`** | `` (пусто) |
| Фильмы | **`category_full`** | `movie` |
🔴 **Фильтр по `component` РАБОТАЕТ** — `main` пропускает, `category_full` режет.
Симптом «в Фильмах снова секции» **не от от фильтра**: переход
`main → category_full → main` даёт **ПУСТОЙ трейс `partNext`** — на этих переходах он
не вызывается вообще, Lampa **переиспользует уже отрисованный пул**.
Отсюда оба симптома одним объяснением:
1. **В «Фильмах» видны секции** — это те же отрисованные элементы; Lampa не
перерисовала страницу при переходе.
2. **При возврате они не сверху** — повторного `wlInject` не было, `partNext` не звался.
🔴 **Вывод: опираться на `partNext` как на ЕДИНСТВЕННУЮ точку входа НЕЛЬЗЯ.**
🔴 **Питфолл метода:** `Lampa.Router.call('main')` как «вернуться на главную» **не
работает** (`ERR: Cannot read property 'url' of undefined`). Навигацию по экранам
проверяет Alex пультом, программно её не воспроизвести.
### Фикс — `wlWatchActivity()`
Хук на `Lampa.Listener.follow('activity')`. На старте activity с `component='main'`
сбрасывается `window.__wl_done = false` — следующий `partNext` вставляет секции заново
(в голову, через `unshift`):
```js
function wlWatchActivity() {
if (window.__wl_activity_hook) return;
window.__wl_activity_hook = true;
Lampa.Listener.follow('activity', function (e) {
if (e.type !== 'start') return;
if (wlComp({component: e.component}) === 'main') {
window.__wl_done = false; // ← снять флаг вставки
if (!window.__wl_menu || !Object.keys(window.__wl_menu).length) wlPrefetch();
}
});
}
```
Плюс **`wlPrefetch()` перестал быть одноразовым** — убран гейт `__wl_prefetched`,
меню обновляется при возврате на главную. Вызов в `startPlugin()`:
`wlPatchPartNext();wlPrefetch();wlWatchActivity();`.
### Сборка и деплой (выполнено)
```
python3 build_plugin.py → 17/17 инвариантов, 19773 байт
node --check plugin-cub.js → SYNTAX OK
docker exec lampac grep -c wlWatchActivity /lampac/module/Catalog/plugin.js → 3
бэкап: catalog/plugin.js.bak-before-activityhook-<ts>
рестарт Lampa на ТВ: --- закрываю --- --- запускаю --- --- готово ---
```
**Инварианты (17 всего).** Добавлены `wlWatchActivity def=1`,
`wlWatchActivity call` — считается по строке `wlPrefetch();wlWatchActivity();`,
🔴 **НЕ по `wlWatchActivity()`** (иначе определение ловится как вызов, счётчик врёт:
первый прогон дал 3 вместо 1 → инвариант упал), `activity hook сбрасывает флаг`.
### Приёмка (ждёт Alex) — полный цикл, а не только первый экран
1. главная → 4 секции Watchlist сверху, **БЕЗ дублей внизу**
2. Фильмы → документальные → секций Watchlist нет
3. **назад на главную → секции СНОВА сверху, снова без дублей** ← пункт, который ломался
✅ **Коммиты СДЕЛАНЫ и запушены** (в `~/Developer/lampac-scripts`, Gitea):
- `5e55a47` — «секции CUB только на главной экране» (фильтр `wlOnMain`)
- `3a35475` — «фильтр по component + предзагрузка меню + хук на activity»
🔴 **Правки обязаны попадать в репу ДО доклада.** В этой сессии `5e55a47` был
закоммичен, а работа по фильтру по `component` осталась незакоммиченной, и Alex
справедливо спросил «Коммит ты сделал текущей работы я надеюсь». Порядок:
`build_plugin.py` правится → собирается → **копируется в `scripts/` → коммит → push**,
и только потом заливка и приёмка. Файлы в `/Users/admin/lampac-tmp/` — НЕ источник.
🔴 **Правило процесса:** Alex не принимает статус-репорты и вопросы вместо результата
(«Ты заебал. Ты блядь задачу решаешь или мозг мне ебешь?!», «Ты делаешь блядь или
опять залип», «Ты все починил? Проверять?») — **доделать и доложить один раз**.
Замер — часть работы, а не отдельная задача на согласование.
🔴 **Правило проверки перед докладом.** Дважды за сессию доклад «готово, проверяй»
был **ложным**: правка физически не доезжала до ТВ (неработающий рестарт приложения).
Перед словом «готово» обязателен замер уникального маркера сборки на ТВ. Alex ловил
это сам («С чего ты взял что лампа перезапущена?», «Ты где его взял? Эта ебанина
где-то записана?») — это не должно повторяться.
---
## 🔴 2026-09-24 (вечер): ПЕРЕПИСЫВАНИЕ — один sync-скрипт вместо отдельного генератора
**Требование Alex:** «перепиши по нормальному чтобы не отдельный скрипт на лампу был
а один sync скрипт и писал сразу lampa feed… обнови синк скрипт чтобы писал фид вместо
добавления закачек в arr, а отдельный lampac script удали».
**Причина (найдено в этой сессии).** `gen_watchlist.py` (Lampac, 878 строк) — ВТОРОЙ,
независимый матчер TMDB: он не читает ни `.resolve_cache.json`, ни маркеры ✅/🗓️/❓.
Годами его связывал с резолвером Radarr неявно (матч по названию → те же версии фильмов,
что выбрал `resolve`). После удаления контента из arr 2026-09-24 мост исчез, и каталог
впервые матчил вслепую → 3 карточки сменили фильм (Олдбой 2003→2013 ремейк,
Unbroken 2003→Несломленный 2014, Shogun 1968→Убийца сёгуна 1980). Лечим не годами
в `.md`, а слиянием матчеров.
**Статус: ✅ РЕАЛИЗОВАНО 2026-09-24 (вечер).** Отказ от отдельного `feed.py`/CLI-шага —
Alex уточнил требование: «это же чистая доработка sync-down?» → **`sync-down` сам пишет фид**.
Отдельного шага `feed` в CLI и в nightly НЕ появилось.
| Что | Как сделано |
|---|---|
| Новый `watchlist_sync/sync_down.py` | ~380 строк вместо 195: `resolve` → `wwwroot/watchlist.json` + `watchlist-<cat>.json` + `cards/card-<movie-NNN>.json` + `cards.json` + `missing.json`; опционально ❓-синк |
| CLI | `sync-down [--apply] [--feed-root]`; `--batch` оставлен deprecated (в фид идёт весь список) |
| Пути | `--feed-root` → env `LAMPAC_ROOT` → `<repo>/../lampac` (fallback для Мак-чекаута). Хардкода UUID-путей в коде нет |
| `arr.py` | 🔴 **УДАЛЁН** (`git rm`, коммит `a524f5d`) — история сохранена в гите. Бэкапы НЕ делались (решение Alex: «в истории гита останется») |
| ⬇️ | **не ставится больше нигде.** Alex: «⬇️ больше ставить не надо, ведь закачки нет». `mark_queued` в `parser.py` оставлен для исторических строк |
| Коммит | `a524f5d` — «sync-down: build Lampa feed instead of adding to arr; drop arr.py» |
**🔴 Три открытия при переносе (стоили трёх итераций — не повторять):**
1. **Фильтры рейтинга/ужасов в фиде НЕ НУЖНЫ.** Я притащил в фид правила закачки
(`rating >= 6.0`, horror-skip) — фид упал до 268 позиций из 392. Это правила
**Radarr** («не качаем то, что не смотрим»), а каталог — не очередь закачки:
старый генератор отдавал horror тоже (Вивариум, Субстанция, Ужасающий, Зубастики
есть в живом фиде). В `sync_down.py` оставлен явный комментарий с запретом
возвращать эти фильтры.
2. **`item.ambiguous` (❓) НЕ исключает позицию из фида.** ❓ означает «название в `.md`
неточное, пригодился бы год», а НЕ «нерезолвлено». Резолвер для таких всё равно
отдаёт кандидата, и старый генератор их публиковал (Альфа, Кибердеревня, Орвилл,
Ножницы — все с постерами в живом фиде). Из фида исключают только: нет `tmdb_id`
и `result.year_ambiguous` (🗓️ — реально два разных фильма).
3. **Фид должен брать лучшего кандидата, а не отказываться.** `resolve()` при
неоднозначности возвращает `status="ambiguous"`, `candidate=None`, но топ-3 лежат
в `all_candidates` — среди них верный. Старый генератор брал первый результат TMDB.
Если строить фид строго по `result.status == "ok"`, теряется **54 позиции**
(девять, Мементо, Babylon, Orville, Матрица-класс названий: Казино, Сияние, Оно,
Отступники, Бешеные псы…).
✅ **РЕШЕНО 2026-09-24:** Alex — «бери лучшего кандидата и пиши вопрос». Фид берёт
`all_candidates[0]`, а название уходит в `missing` → ❓ в `.md` (год, добавленный
после ❓, пиннит позицию на следующем прогоне). Реализовано в `sync_down.py`.
**🔴 ГЛАВНЫЙ БЛОКЕР, вскрытый переносом (не закрыт):** резолвер проекта матчит
**хуже** старого генератора. Сверка по TMDB-номеру (не по названию — в фиде имена
из TMDB, в `.md` из строки, сравнение по имени бесполезно):
```
было в живом фиде, в новом с ДРУГИМ tmdb_id: 86 позиций
примеры: Злая: Часть 2, Разделение, Дракула, Ван Хельсинг, Судья Дредд, Дюна-класс
плюс мусорные матчи по кривым строкам: «[Убийственная вечеринка] (2025)» → фильм 2007
```
**Причина:** в старом генераторе 4 ступени резолва — Radarr → TMDB → **КП-по-номеру
из ссылки `kinopoisk.ru/film/N/`** → КП-по-названию. Самая надёжная ступень
(номер из ссылки — машинный ключ, не зависит от переименований) в `resolver.py`
**отсутствует вообще**. До переноса это маскировал Radarr-фастпатч; после удаления
контента arr маскировка исчезла и расхождение вылезло.
**Варианты (ждёт решения Alex):**
- **А (рекомендован):** перенести в `sync_down`/`resolver` ступень «КП-по-номеру из
ссылки» — код разобран и лежит в бэкапе `gen_watchlist.py` (`kp_search_by_id` +
`_docs_to_tmdb` с `_by_id`). ~30 мин, проверяется тем же diff по tmdb_id.
- **Б:** выкатить как есть и вычистить 86 расхождений руками — вернутся после прогона.
**Замеры dry-run по ходу работы:** 268 (с фильтрами) → 332 (без фильтров, `ambiguous`
исключается) → **397** (`all_candidates[0]` + ❓; больше 392, потому что старый
генератор терял часть позиций на Radarr-фастпатче и ❓-строках).
На живой фид НЕ выкатывалось: сначала matcher-parity.
**Реперные метрики живого фида на 2026-09-24** (для сверки после переноса):
```
каналы: new 32 / films 346 / cartoons 7 / anime 7 → карточек 392
источники: Radarr 0, TMDB 371, автопоиск по названию 17,
по номеру из ссылки 4, без TMDB 30, постеров 390
```
🟡 **Рабочее правило (Alex):** код писать сразу в проекте на Маке
(`~/Developer/watchlist-sync/`), `/tmp` — только для чтения чужих файлов и одноразовых
выборок. Мак-репа и Кракен держатся на одном коммите (`4266e37` на момент старта).
**Границы (что не трогаем):** `catalog/sites/watchlist.yaml` (контракт Lampac:
`plugin: watchlist`, категории, `card_parse`/`card_args` с `format`-выражениями),
`catalog/plugin.js` (инжект секций в CUB через `Api.partNext`), структура
`cards/card-movie-NNN.json`. Механика `missing.json` → ❓ осталась, переехала внутрь
`sync_down.py` (`_sync_question_marks`, двусторонняя — снимает протухший ❓).
**🔴 Незакрытое на конец сессии 2026-09-24 (для следующей сессии):**
1. **Matcher-parity (ГЛАВНОЕ)** — см. «ГЛАВНЫЙ БЛОКЕР» выше: перенести ступень
«КП-по-номеру из ссылки» в резолв фида (вариант А) либо вычистить 86 расхождений
руками (вариант Б). Без этого выкат на живой фид покажет не те фильмы на ~86 позициях.
2. Возврат `sync-down` в `watchlist-nightly.sh` (строка 40 сейчас «ПРОПУЩЕН»;
в 02:30 крутится отдельный `watchlist-catalog.sh`) + удаление `watchlist-catalog.sh`
и строки крона `30 2 * * *`.
3. Удаление `lampac/scripts/gen_watchlist.py` (878 строк) и `mark_missing.py` (179).
🔴 НЕ удалять до закрытия п.1: оттуда берётся код `kp_search_by_id` / `_docs_to_tmdb`.
4. Прогон `--apply` на живой фид + `docker restart lampac` (сброс кэша Catalog)
+ приёмка в UI Lampa.
5. Правка `sync_down.py`: ветка «не нашлось» (`not_found`) обязана добавлять позицию
в `missing` — за это отвечает `_sync_question_marks` (двусторонний ❓).
---
> Реализовано 2026-09-22. План был переписан после разведки кода модуля Catalog.
> Базовое развёртывание — [[personal/plans/lampac-kraken]] (шаги 1–6 закрыты).
>
> 🔴 **АРХИТЕКТУРА ПРИЗНАНА НЕВЕРНОЙ 2026-09-22 — переписывается.**
> Сделано `menu` (подмена родных разделов) вместо `main` (секции СВЕРХУ).
> Плюс источник данных — парсинг текстового `.md` вместо **Radarr API**, где уже
> есть TMDB ID и постеры. Alex: «Я предполагал что будет отображаться то что и раньше
> (список cub) но В НЕГО будет сверху добавлено несколько секций в которых будут
> РЕАЛЬНЫЕ ЭЛЕМЕНТЫ - а не сгенерируют пустые заглушки!»
> См. «🔴 АРХИТЕКТУРНАЯ ОШИБКА» — читать ПЕРВЫМ.
>
> **Итог одной строкой:** серверная часть каталога работает полностью — «Мой список»
> первым в `/catalog`, 4 категории (403 позиции, 348 постеров), **карточка по клику
> отдаёт все поля: 352/352 с числовым `tmdb_id`**. Родной CUB не тронут.
> Осталась приёмка глазами на ТВ.
>
> **Обновление 2026-09-22 (четвёртая сессия): 🔴🔴 НАЙДЕНА ИСТИННАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО» — КИРИЛЛИЦА В КЛЮЧЕ КАТАЛОГА.**
> Все предыдущие фиксы (числовой `tmdb_id`, права, macOS-tar) были **не той причиной**.
> `tcpdump` на Кра́кене при клике на ТВ показал реальный запрос:
> `GET /catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=... → 400 Bad Request`.
> **`plugin=Мой список`** — кириллица. `ModInit.goInit` строки 51-52 чистит имя регуляркой
> `[^a-z0-9\-]`, то есть **вырезает всю кириллицу целиком** → `""` → `File.Exists("sites/.yaml")` = false →
> fallback `FindSiteByDisplayName("")` тоже мимо (сравнивает уже с пустой строкой) → `null` →
> `BadRequest("init not found")` → **400** → «Здесь пусто».
> **ФИКС:** явный `plugin: watchlist` в `watchlist.yaml` (переопределяет ключ по
> `ApiController.cs:136`); `displayname: Мой список` остаётся надписью в UI. Ключ каталога стал `watchlist`.
> Проверено: `plugin=Мой список` → **400**, `plugin=watchlist` → **200** с полными данными.
> 🔴 **Урок диагностики:** проверять карточку curl'ом надо ТЕМ ЖЕ ключом, который шлёт ТВ
> (я подставлял латиницу руками → 200 → ложный вывод «проблема в клиенте»). Детали —
> [[personal/tech/lampac-deployment]] раздел «🔴🔴 ГЛАВНАЯ ПРИЧИНА».
>
> **⚠️ ЗАДАЧА СЕССИИ (требование Alex): ✅ ВЫПОЛНЕНО — СЕКЦИИ ВНЕДРЯЮТСЯ В CUB-ФИД.**
> Дословно требование: «надо блядь добавить в CUB список новые секции а не ручное
> блядь переключение!». Реализовано в `plugin-cub.js` через **обёртку
> `Lampa.Api.sources.cub.main`**: наши секции **добавляются** в массив
> `parts_data` CUB-фида (не подменяют раздел), карточкам проставляется
> `source: 'watchlist'`. Вызов — по событию `app:ready`.
> Залито, отдаётся в сборке `/catalog.js` (35566 Б). **Ожидает приёмки в UI.**
> Механика — [[personal/tech/lampac-deployment]] раздел «Внедрение секций Watchlist в CUB-фид».
>
> **Обновление 2026-09-22 (ВОСЬМАЯ СЕССИЯ): ✅✅ ЭТАП «СЕКЦИИ ВНУТРИ CUB» ЗАКРЫТ — ФИД СМЕРЖИЛСЯ.**
> Alex: «Отлично, фид смержился. Закрываем этот этап в плане.»
>
> **Корень, который держал этап** (нашёл только по коду `app.min.js` @1292588):
> ```js
> function partNext(parts, parts_limit, partLoaded, partEmpty) {
> var pieces = parts.filter(function (p) {
> return typeof p == 'function';
> }).slice(0, 0 + parts_limit); // ← limit = 6, ЖЁСТКИЙ СРЕЗ ГОЛОВЫ
> ```
> `cub.main$1` кладёт в `parts_data` **28 родных секций** и вызывает `partNext(parts, 6, …)`.
> Наш инжект делал `parts.push()` → секции уходили в **хвост** (позиции 29–32) → `slice(0, 6)`
> их отрезал. Трасса пула на живом ТВ: `before: 28 → after: 32`, в DOM — **ни одной** нашей секции.
>
> **ФИКС: `parts.unshift()` вместо `parts.push()`** — в голову пула, в срез, рисуются СВЕРХУ
> (ровно требование Alex). Порядок меню обходится **в обратном порядке**, потому что каждый
> `unshift` сдвигает предыдущие вправо.
>
> **Ложный след, потративший время:** тест `g: {wlSection: "undefined", …}` читался как
> «функции недоступны из замыкания». На самом деле инжект **внутри** замыкания плагина и
> `wlSection` виден перехватчику — проба смотрела снаружи. Доказательство: прямой вызов
> `partNext(empty,…)` дал `len: 4`. **Проверять пул ТРАССОЙ (`before/after`), а не наличием
> функций на `window`.**
>
> **Приёмка (серверная часть): 18/18.** `/catalog.js` 33550 Б с `unshift`; `/catalog` →
> ключ `watchlist`, menu 4 категории; список: new 33/30 постеров, films 360/308/312 ID,
> cartoons 3/3, anime 7/7; карточка `tmdb_id: 424781` числом; регресс-тест `plugin=Мой список`
> → **400** (кириллица по-прежнему не проходит).
>
> **Артефакты:** `build_plugin.py` (+инварианты `wlInject def`, `unshift (не push)`,
> `push в INJECT (должно 0)`), `verify_unshift.py` (проверка фикса в рантайме БЕЗ деплоя),
> `full_check.py` (полный цикл сервер+ТВ), `tv_accept.py`, `diag_trace.py`, `diag_calls.py`.
> Бэкапы: `plugin.js.bak-20260922-165613` (последний рабочий), `plugin-cub.js.bak-*`.
>
> 🔴 **Питфолл рантайм-проверки:** повторная загрузка `/catalog.js` скрипт-тегом НЕ перезапускает
> `startPlugin` (флаг `window.plugin_catalog` уже стоит) → `patched: false`. Для быстрой итерации
> без деплоя оборачивать `Lampa.Api.partNext` вручную с `unshift` (`verify_unshift.py`).
> `location.reload()` на webOS догружает новый `catalog.js` **дольше 30 с** — ждать до 120 с.
> **Обновление 2026-09-22 (седьмая сессия): 🔴🔴 ПРИЧИНА «В CUB НИХУЯ НЕ ПОМЕНЯЛОСЬ» — `Manifest` НЕДОСТУПЕН В ОБЁРТКЕ.**
> Alex: «Ты вообще нихуя не поменял!!». Проверка через **CDP-пробу на живом ТВ** подтвердила
> его правоту: обёртка `Api.sources.cub.main` стояла (`__wl_injected: true`), но вызов
> `cub.main({})` возвращал **4 родные секции** и **ни одной нашей**.
> **Корень:** первая версия инжекта читала `Manifest.catalogs['watchlist']` напрямую, а
> `Manifest` объявлен **локально в замыкании плагина** → `ReferenceError: Manifest is not defined`
> → `catch` → молча отдавался родной фид. **Ошибки нет ни в UI, ни в логах.**
> **Фикс:** `window.__wl_menu` (заполняется в `loadCatalogs`) + `wlEnsureMenu()` с фолбэком
> на запрос `/catalog`. Переписан сборщик: **`build_plugin.py`** (детерминированный,
> с инвариантами `INJECT=1`/`def=1`/`startPlugin=1`, `node --check` + баланс скобок),
> вместо неидемпотентного `patch_cub_inject.py` (он вставлял INJECT дважды и ломал скобки).
> Залито (сборка **35440 Б**), **ждёт перезапуска Lampa на ТВ** — `docker restart lampac`
> НЕ перезагружает плагин в памяти ТВ.
> Механика + CDP-пробы — [[personal/tech/lampac-deployment]] раздел
> «🔴🔴 ШЕСТАЯ СЕССИЯ: ИСТИННАЯ ПРИЧИНА…».
> **Обновление 2026-09-22 (пятая сессия): ✅✅ ПРИЁМКА ALEX ПРОЙДЕНА — ФИЛЬМЫ ОТКРЫВАЮТСЯ И ПРОИГРЫВАЮТСЯ.**
> Дословно: «фильмы из списка Watchlist теперь открываются и проигрываются».
> Закрыт последний блокер — кириллический ключ плагина (`plugin: watchlist`).
> Плюс устранён питфолл `Staticache` (кэш собранного `/catalog.js` переживал
> заливку и выглядел как «патч не применяется» → `docker restart lampac`).
>
> **Осталось (2026-09-22, конец СЕДЬМОЙ сессии):**
> 0. ✅ **ЗАКРЫТО 2026-09-22 (восьмая сессия):** `plugin-cub.js` залит на Кра́кен,
> фикс `unshift` вместо `push` — **фид смержился**, секции Watchlist рисуются
> внутри главной CUB рядом с родными. См. блок «ВОСЬМАЯ СЕССИЯ» выше.
> 1. **5 фильмов с Кинопоиск-ID** (`film/NNNNN/`) — «Рассекая волны: экспедиция „Лас Бальсас“»,
> «Хамнет», «Эхопраксия», «Цена вершины», «Астрал. Могильный ритуал 2», «О добре и зле»,
> WeWork (2024).
> TMDB не нашёл (2025 год) → нет карточки и постера → клик даёт «здесь пусто».
> **Нужен выбор Alex:** заглушка с кнопкой поиска торрента либо дорисовать `tmdb_id`.
> 2. **~51 название без постера** — проблема ЗАПИСИ В СПИСКЕ, не скрипта:
> `Плохие парни 1/2` (ДВА фильма одной строкой), `Солярис Тарковский` (уточнение
> режиссёра), `Самолетом, поездом, машиной (1987)` (русский аналог).
> Генератор такое угадать не может — **правится только список руками**.
>
> **Обновление 2026-09-22 (четвёртая сессия, ранее): ✅ ЧИСЛОВОЙ `tmdb_id` В КАРТОЧКАХ.**
> 352 из 352 карточек отдают `tmdb_id` числом, `source`, `runtime`, `vote_average`, жанры.
> Механика: `ModInit.cs:285 nodeValue` возвращает любой `JValue` СТРОКОЙ → в `card_args`
> нужен `format: "long.TryParse(value, out long v) ? v : 0"` (`format` = CSharpEval,
> `CatalogNodeValue(value, host)`). Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ и
> переживает `docker restart` — после правки данных обязателен рестарт И проверка на
> НЕзакэшированном uri. Детали — [[personal/tech/lampac-deployment]].
>
> **Обновление 2026-09-22 (третья сессия): ✅ КАРТОЧКА ПОЧИНЕНА ОКОНЧАТЕЛЬНО.**
> «Здесь пусто» давало отсутствие **числового** `tmdb_id`: `plugin.js` делает
> `if(json.tmdb_id)`, а `ModInit.cs:285` возвращает любое `JValue` СТРОКОЙ.
> Фикс — `format: "long.TryParse(...)"` в `card_args`. Плюс кэш `catalog:card:*`
> в ПАМЯТИ переживал `docker restart` и отдавал старое. См. «🔴 ФИНАЛЬНЫЙ ФИКС».
>
> **Обновление 2026-09-22 (вечер):** Alex лично нашёл источник в UI
> («остальное → основной источник → Мой список»), увидел результат и подтвердил
> диагноз: секции подменяют CUB, нет превью, элементы — сырые строки файла.
> Затем сам сформулировал целевую схему (Radarr = источник метаданных **без автоскачки**,
> просмотр через TorrServer, файлы на HDD не пишутся) → записано в **«ПЕРЕДЕЛКА 2»**.
> План работ на апруве, **ничего не выполнено**.
>
> **Обновление 2026-09-22 (поздний вечер): ✅ ШАГИ 0–4 ПЕРЕДЕЛКИ 2 ВЫПОЛНЕНЫ.**
> Автоскачка на HDD **выключена фактически**: Radarr (45→**0** monitored, очередь 6→**0**,
> RSS 30→**0**) и Sonarr (13→**0**, 19→**0**, 15→**0**). Метаданные целы — 50 фильмов,
> **46 с постерами**, 24 файла на диске не тронуты. Проверено: Transmission не сдвинулся
> ни на байт за 45 с, логи Radarr пусты. Бэкапы `radarr.db`/`sonarr.db`/`config.xml` — метка
> `20260922-123404`. См. «✅ Порядок работ» и «🔴 Питфоллы выполнения».
>
> **Обновление 2026-09-22 (ночь): 🔴 ПОПЫТКА УБРАТЬ `menu` ПРОВАЛЕНА — ГИПОТЕЗА БЫЛА НЕВЕРНА.**
> Попытка «оставить `main`, убрать `menu`» **сломала канал**: «Мой список» исчез из `/catalog`,
> `catalog/list?plugin=watchlist` отдавал пустоту. Три раунда правок YAML, три рестарта.
> **Установлено тестом:**
> - **`menu` ОБЯЗАТЕЛЕН** — он регистрирует канал, а не подменяет разделы
> (`ApiController.cs:70`: без `menu` сайт отбрасывается). Убрать нельзя.
> - **блок `main` в YAML НЕ СУЩЕСТВУЕТ** — любые формы (список/словарь) ломают парсинг,
> `goInit` → null, сайт молча пропадает. **Errors в логах нет.**
> - **`main` Catalog генерирует САМ** из `menu` — в JSON-ответе `/catalog` есть оба ключа
> с одинаковым содержимым.
> - **Подмену разделов делает `plugin.js`**, не YAML (`Listener.follow('menu')` + `e.abort()`),
> срабатывает только при выбранном источнике «Мой список».
> - **`host` + `uri` СКЛЕИВАЮТСЯ** (`ListController.cs:53`) → `uri` обязан быть относительным.
> Также найден питфолл: **явный `format` в `menu` перезагружает страницу** при выборе раздела.
>
> **Текущее состояние:** каталог **рабочий** (waтchlist.yaml откачен к рабочей форме,
> «Мой список» на месте, 4 категории 360/33/3/7). Правки 5–8 (постеры через `resolver.py`,
> `plugin.js`, `arr.py`) **не начаты** — ждут решения Alex по вопросу
> «CUB и Мой список одновременно». См. **«🔴 ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН»**.
>
> **Аудит «как это реализуемо» — ✅ ЗАВЕРШЁН 2026-09-22.** Проверено всё:
> TorrServer **на диск не пишет** (`UseDisk: false`, RAM-кэш 96 МБ) — главный вопрос закрыт;
> поиск торрентов работает через **Jackett-API самого Lampac** (отдельный Jackett не нужен);
> найдены **оба** виновника автоскачки — Radarr (45 monitored, очередь 6) и
> **Sonarr** (13 monitored, очередь 19). См. «✅ ПРОВЕРКА "как это реализуемо"».
> ⚠️ Порядок работ теперь включает Sonarr — раньше он в плане не учитывался.
>
> **Обновление 2026-09-22 (ночь, вторая сессия): 🎯 НАЙДЕНА НАСТОЯЩАЯ ПРИЧИНА «пустого каталога» + артефакты готовы.**
> `Фильмы` (первая категория в `menu.categories`) возвращала **360 элементов из `watchlist.json`**
> вместо `watchlist-films.json` — то есть игнорировала блок `format`. Отсюда «здесь пусто»:
> элементы шли с `id` = ссылка на поиск Кинопоиска и `img: null`.
> **Фикс: первая категория в `menu.categories` — это «Новинки», не «Фильмы».**
> Все 4 категории проверены: films/new/cartoons/anime → правильные файлы.
> Написаны: `gen_watchlist_v2.py` (постеры через TMDB + Radarr; первый прогон
> **330 постеров из 403**), `patch_plugin.py` (+`enumerable:true`, `node --check` чист),
> `deploy_lampac.py` (заливка + проверка одной командой), обновлённый `docker-compose.yml`
> (volume на `plugin.js`). Правка `arr.py` применена и **закоммичена `9872016`**.
> См. **«🟢 РЕШЕНИЕ: первая категория игнорирует format»** — читать ПЕРВЫМ.
>
> **Обновление 2026-09-22 (ночь, третья сессия): ✅ ДЕПЛОЙ ПРОШЁЛ, ПОСТЕРЫ И ПОИСК В UI ЕСТЬ. КАРТОЧКА В РАБОТЕ.**
> Всё залито на Кра́кен и **проверено на живом сервере**:
> - «Мой список» первый в `/catalog`, `main` = `[Новинки, Фильмы, Мультфильмы, Аниме]`;
> - **films 360 / постеров 308**, new 33/30, cartoons 3/3, anime 7/7;
> - `id` элементов = **`movie/424781`** — рабочие TMDB-карточки (не поиск Кинопоиска);
> - патч `plugin.js` **применён** (`enumerable:true`, файл 10738 Б), volume смонтирован —
> проверено `docker inspect`, переживёт обновление образа;
> - `arr.py` фикс в бою, Radarr/Sonarr monitored 0, RSS 0, очередь 0.
>
> 🔴 **ОСТАЛОСЬ ОДНО: карточка по клику.** Была HTTP 500 → сейчас **HTTP 400 `parse.node`**.
> Корень найден по коду: `CardController.Index(plugin, uri, type)` — **параметра `cat` НЕТ**
> (поэтому `{cat}` в `initUrl` пустой → `cards-.json` → 404 → 500), а **`parse.node` —
> статичный JSONPath** (`json.SelectToken(parse.node)`, строка 95), подстановок не делает,
> и `/` в пути = вложенность.
>
> **Решение дописано, но НЕ залито:** файл на каждый фильм `wwwroot/cards/card-movie-NNN.json`
> с обёрткой `{"card": {...}}`, `initUrl` собирает путь C#-выражением
> `$"{host}/cards/card-{uri.Replace("/", "-")}.json"`, `node: card`.
> `deploy_lampac.py` пока НЕ умеет лить каталог `cards/` — доклеить `scp -r`.
> См. **«🔴 КАРТОЧКА: механика card_parse»**.
> **Обновление 2026-09-22 (ночь, четвёртая сессия): ✅ КАРТОЧКА ПОЧИНЕНА. 339/339 HTTP 200. СЕРВЕРНАЯ ЧАСТЬ ЗАКРЫТА.**
> Схема «файл на фильм» залита и **проверена на живом сервере**:
> - `wwwroot/cards/card-movie-NNN.json` (345 файлов), обёртка `{"card": {...}}`,
> владелец `1000:1000`, режим `644`;
> - `card_parse.initUrl` = C# `return $"{host}/cards/card-{uri.Replace("/", "-")}.json";`,
> `node: card` (статичный — потому и нужен файл-на-фильм);
> - **все 339 карточек с TMDB-ID отдают HTTP 200**, имена читаются
> («Я буду жить», «На цепи», «Простите за беспокойство»);
> - `deploy_lampac.py` дополнен шагом **2b** (заливка каталога `cards/` через tar)
> и проверкой карточек в шаге 7.
>
> **Итог каталога:** Новинки 33/30, Фильмы 360/308, Мультфильмы 3/3, Аниме 7/7 —
> **403 позиции, 348 постеров, 352 рабочих карточки**, постеров-подложек нет.
>
> 🔴 **Три причины поломки, все устранены** (детали — «🔴 ПИТФОЛЛЫ, найденные по ходу»):
> 1. **права**: tar распаковывал от root → `root:root 600` → dotnet `Permission denied` →
> статика молча **404** (выглядит как «файла нет»);
> 2. **macOS AppleDouble**: BSD `tar` клал `._*.json` в `wwwroot` → нужен `--no-mac-metadata`;
> 3. 🔴 **ложный диагноз — потерянный час**: карточку проверял по **`/card`**, где всегда 404.
> Правильный путь — **`/catalog/card?plugin=watchlist&uri=movie/NNN`**.
>
> **ОСТАЛОСЬ: приёмка глазами на ТВ** (`192.168.1.70`): секции сверху с постерами,
> клик → карточка с описанием → воспроизведение через TorrServer.
> **Приёмка = результат виден в UI**, не «API отвечает».
>
> **Не тронуто, ждёт решения Alex:** 310 фильмов из списка, которых нет в Radarr
> (51 без постера, идут через фолбэк Кинопоиска). Без апрува не делать.
---
## 🟢 ВОСЬМАЯ СЕССИЯ 2026-09-25: ФИЛЬТР «СЕКЦИИ ТОЛЬКО НА ГЛАВНОЙ» (PLAN)
**Задача Alex:** «watchlist фид отображается не только на главной, но даже когда я
выбираю категорию например "документальные" в разделе Фильмы» + «он как будто долго
прогружается относительно основного CUB списка». Требование: **ТОЛЬКО ГЛАВНЫЙ ЭКРАН**
(белый список, не чёрный — Alex: «В смысле всё кроме? Только главный экран!»).
### Диагноз (по коду, замеры)
1. **Причина «на любом экране».** `plugin-cub.js:164` — `wlPatchPartNext` вставляет
секции БЕЗУСЛОВНО:
```js
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
var menu = window.__wl_menu;
if (menu && Object.keys(menu).length) { parts.__wl_done = true; wlInject(parts, menu); }
```
`Lampa.Api.partNext` — глобальный: через него строят страницу И `main()`
(`plugin.js:30`), И `category()` (`plugin.js:36`). Никакой проверки активного
экрана нет → `unshift` кладёт секции в голову пула на КАЖДОМ экране.
Фильтра по activity в коде НЕ БЫЛО — это не регресс, а исходное отсутствие.
2. **Замеры скорости (Kraken, `127.0.0.1:9118`):**
- `catalog/list?cat=films` прогретый кэш — **4 мс**; первый холодный — **2 250 мс**
(`hybridCache` модуля Catalog парсит JSON целиком при первом запросе)
- `watchlist-films.json` (чистая статика) — 1 мс
- 🔴 `catalog/card?uri=movie/424781` — **66 118 мс (66 сек!)** на 939 байт.
Это НЕ диск и не фид: `card_parse.initUrl` исполняется как C# и ждёт внутри
CardController. Отдельный баг, в эту правку не входит.
- ⚠️ `curl -w %{size_download}` показывает размер **после** распаковки gzip —
на этом легко обмануться: `watchlist-films.json` = 88 КБ несжатых при 347
позициях, а `cards.json` = **548 КБ**. Реальный объём фида в разы больше «видимого».
### Решение
Белый список по активному activity Lampa:
```js
function wlOnMain() {
try {
var a = Lampa.Activity && Lampa.Activity.active && Lampa.Activity.active();
var act = (a && a.activity && a.activity.url) ? a.activity.url : '';
return act === 'main'; // ТОЛЬКО главная
} catch (e) { return false; }
}
```
Плюс `parts.__wl_done = true` ставить ТОЛЬКО при фактической вставке (иначе после
возврата из категории на главную секции не доберутся). `console.log` при пропуске —
для отладки на живом ТВ.
### Шаги
1. правка `plugin-cub.js` + `build_plugin.py` (INJECT + новый инвариант `wlOnMain`)
2. `python3 build_plugin.py && node --check plugin-cub.js`
3. бэкап боевого `catalog/plugin.js` → `scp` нового
4. **перезапуск Lampa на ТВ обязателен** (`tv_restart.sh` через `luna-send`;
`docker restart lampac` кэш webOS не сбрасывает)
5. 🔴 приёмка = **видно в UI**: главная — 4 секции сверху; Фильмы → документальные —
секций НЕТ. CDP-проба читает `Lampa.Activity.active()` на обоих экранах.
### ✅ ИТОГ 2026-09-25: фильтр РАБОТАЕТ, закоммичено
- **Коммит `5e55a47`** в репозитории **`~/Developer/lampac-scripts`** (Gitea
`git_admin/lampac-scripts`), запушен в `origin/main`. Туда переехали
`scripts/build_plugin.py` + `scripts/plugin-cub.js` — **из `lampac-tmp`, который
вне git**. `lampac-tmp` больше не источник истины.
- Приёмка на живом ТВ (CDP): **`act_url = "movie"` (экран Фильмов) → секций Watchlist
в DOM НЕТ.** Именно то, что просил Alex.
- Две правки по факту замеров, обе стоили итерации:
1. **`url` читается с ВЕРХНЕГО уровня** — `active().url`, НЕ `active().activity.url`
(там его нет вообще: `{component, object, slide, body, is_stopped, is_started}`).
Первая версия читала `.activity.url` → всегда `undefined` → секции исчезли → «Нету».
2. **Пустой `url` + пустой `Manifest.catalogs` = инициализация → пропускаем.**
Иначе строгая проверка отрезала единственный вызов `wlMenu()`, добирающий меню
после сбоя `/catalog`.
- 🔴 **Питфолл замеров:** CDP-проба сразу после рестарта Lampa даёт `ready=false`,
`plugin=false`, `patch=false`, `act_url=ERR` — это НЕ поломка, плагин ещё грузится.
Мерить надо через ~30 с, иначе легко уйти в ложную диагностику «плагин не подъехал».
- 🔴 **Задержка секций — архитектурная, в работу ещё НЕ внесена.** Замер: `menu`
заполнен, а `Manifest.catalogs = NOMANIFEST`. Цепочка: главная CUB рисуется своим
`cub.main()`, родные секции стартуют сразу; первый `partNext` видит пустой
`__wl_menu` → уходит в **асинхронный** `wlMenu()` → пока летит запрос, родные секции
отрисованы, `slice(0, parts_limit)` уже отработал → наши 4 секции попадают в пул
только в следующем раунде. **План:** тянуть `/catalog` в `startPlugin()`
**параллельно** с `loadCatalogs()`, чтобы `__wl_menu` лежал в памяти к первой отрисовке.
### Питфоллы, найденные при расследовании (не повторять)
- `ssh kraken` на каждом вызове печатает `bash: line 1: ifconfig: command not found` —
это безвредный шум из `~/.bashrc`, НЕ ошибка команды.
- 🔴 `execute_code` в этой сессии падает на `import` (`No module named 'psutil'`) —
работаем через `terminal` + `read_file` + `jq`, БЕЗ python.
- Обрыв `scp` из-за не найденного пути: `wwwroot/catalog.js` НЕ существует (в wwwroot
лежат только JSON фида) — `plugin.js` живёт в `lampac/catalog/plugin.js` и
монтируется в `/lampac/module/Catalog/plugin.js`.
## 🔴🔴 СЕДЬМАЯ СЕССИЯ: РАБОЧИЙ МЕХАНИЗМ НАЙДЕН — ПЕРЕХВАТ `Lampa.Api.partNext`
**Статус: ✅ секции Watchlist рендерятся ВНУТРИ главной CUB. Проверено на живом ТВ через CDP.**
Alex: «В cub секции сейчас смотрят, рекомендуем, последнее добавленное, огонь, в высоком
качестве. Все.» и «до сих пор хамнет, рассекая волны, эхопраксия, WeWork итд — без постера
и "здесь пусто"» + «Ты вообще нихуя не поменял!!».
### Итог проверки на ТВ (CDP, `probe_ui.py`)
```
ЛЕВО (1) → главная CUB, activity: "main"
titles: ["Простите за беспокойство", "Смерть единорога",
"Рассекая волны: экспедиция «Лас Бальсас»", "Я ругаюсь", "F1",
"Мастерская Сатаны", "Хамнет: История, вдохновившая «Гамлета»", ...,
"Эхопраксия", "WeWork создание и крах компании", ...]
```
То есть **наши фильмы физически в DOM главной CUB** — те самые, что Alex называл.
### Три бага подряд — почему «ничего не поменялось»
**❌ Баг 1 (главный): обёртка `Api.sources.cub.main` не работает В ПРИНЦИПЕ.**
Родной `main$1()` (в `app.min.js`, @1257163) внутри замыкания Lampa зависит от
**локальных** `permit`, `get$7`, `Lang`:
```js
function main$1() {
var parts_limit = 6;
var parts_data = [
function (call) { get$7('?sort=now_playing', params, ...); }, // ← get$7 локальная
function (call) { if (permit.child_small) return call(); ... } // ← permit локальная
];
...
}
```
При вызове извне замыкания (`cub.main({})`) он падает на `permit.child_small`
(`permit` → **undefined** в глобальном контексте) и **никогда не зовёт callback** →
вечный `pending`. Проверено CDP-пробой: `{"pending":true}` через 15 секунд, ни одного fetch.
**Обёртка мертва by design** — не потому что код плохой, а потому что метод невызываемый извне.
**❌ Баг 2: `Manifest` / `Utils` / `network` недоступны вне замыкания плагина.**
Замер через CDP (`probe_globals.py`):
```json
{"Manifest_global":"undefined","Lampa":"object",
"Utils":"undefined","network":"undefined"}
```
Все три — **локальные переменные плагина**. Первая версия инжекта читала
`Manifest.catalogs['watchlist']` напрямую → `ReferenceError` → `catch` → молча
отдавался родной фид. **Ошибки нет ни в UI, ни в логах.**
**❌ Баг 3: `Utils.account()` без `Manifest.api_host` подставляет `{localhost}`.**
```js
Lampa.Utils.account(menu[name]) // menu[name] = "/catalog/list?plugin=watchlist&cat=new"
```
→ `{localhost}/catalog?uid=...&token={token}` → в логе ТВ:
```
Request GET time: 4ms error of {localhost}/catalog?uid=...&token={token}
: Нет подключения к сети
```
`Utils.account` **требует** `Manifest.api_host` в области видимости. Вне замыкания его нет.
### ✅ РЕШЕНИЕ: перехват `Lampa.Api.partNext`
`partNext` — **глобальный** API (`Lampa.Api.partNext`), которым сам CUB собирает главную:
```js
function partNext(parts, parts_limit, partLoaded, partEmpty) {
var pieces = parts.filter(function (p) { return typeof p == 'function'; })
.slice(0, 0 + parts_limit);
...Progress().append(pieces).start(...)
}
```
CUB передаёт туда `parts_data` — **массив функций-секций**. Перехватив `partNext`,
наши секции добавляются **в тот же пул**, что и родные: работают на любой странице,
независимо от источника, без `permit` и без `params`.
```js
function wlPatchPartNext() {
var orig = Lampa.Api.partNext;
Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
var menu = window.__wl_menu;
if (menu && Object.keys(menu).length) {
parts.__wl_done = true; // идемпотентность
Object.keys(menu).forEach(function (name) {
parts.push(wlSection(name, menu[name]));
});
}
}
return orig.apply(this, arguments);
};
}
```
Плюс обязательные спутники (глобальные только!):
- `wlHost()` — `Lampa.Manifest.api_host`, фолбэк хардкод `192.168.1.14:9118`
- `wlAbs(url)` — склейка host + относительный путь **вручную**, `Utils.account` НЕ использовать
- `wlGet(url, ok, fail)` — `new Lampa.Reguest().silent(absoluteURL, ...)`, фолбэк `fetch`
- `window.__wl_menu` — заполняется в `loadCatalogs` при загрузке `/catalog`; фолбэк — сетевой запрос
### Сборщик `build_plugin.py` (детерминированный, заменяет `patch_cub_inject.py`)
```bash
cd /Users/admin/lampac-tmp
python3 build_plugin.py # plugin-patched.js + INJECT → plugin-cub.js
node --check plugin-cub.js # обязательная проверка
```
Инварианты на выходе (скрипт сам их проверяет и падает при нарушении):
`INJECT=1`, `def wlPatchPartNext=1`, `startPlugin=1`, `network.silent в INJECT=0`.
🔴 **Почему не `patch_cub_inject.py`:** он был неидемпотентен — при повторном запуске
вставлял INJECT **дважды** и ломал баланс скобок (`SyntaxError: Unexpected token '}'`),
потому что проверки шли по уже модифицированной строке. `build_plugin.py` режет файл
детерминированно: `[до startPlugin] + INJECT + [новый startPlugin] + '})();'`.
### 🔴 CDP-пробы на живой ТВ (новый инструментарий)
ТВ LG отдаёт **Chrome DevTools Protocol** на порту **9998**:
```bash
curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl'
# ws://192.168.1.70:9998/devtools/page/<ID>
```
Клиент на Mac — `websocket-client` (модуль `websockets` НЕ установлен):
```python
import websocket
ws = websocket.create_connection(WS_URL, timeout=25)
ws.send(json.dumps({"id":1,"method":"Runtime.evaluate",
"params":{"expression":EXPR,"returnByValue":True,"awaitPromise":True}}))
```
Скрипты в `/Users/admin/lampac-tmp/`:
| Скрипт | Что проверяет |
|---|---|
| `probe_tv.py` | базовое состояние: `source`, `hasCub`, `cubKeys`, `wlInjected`, `sourcesList` |
| `probe_globals.py` | доступность `Manifest`/`Utils`/`network` в глобальном контексте |
| `probe_cache.py` | откуда ТВ берёт `catalog.js` (script-теги, кэш) |
| `probe_cub_main.py` | что отдаёт обёрнутый `cub.main` (сколько секций) |
| `probe_inject.py` | инжект нового `plugin-cub.js` в рантайм БЕЗ деплоя (быстрая итерация) |
| `probe_wait.py` | ждёт завершения вызова + перехватывает `console.log` |
| `probe_ui.py` | **финальная приёмка:** переход на главную + чтение заголовков из DOM |
| `restart_lampa.sh` / `tv_restart.sh` | перезапуск Lampa на ТВ через `luna-send` |
🔴 **Питфолл `luna-send` через ssh:** аргументы JSON не проходят через
`ssh 'script -q -c "luna-send ... -a {\"id\":...}"'` — luna-send печатает help
(«send specified appId in message (default is none)»). **Фикс:** положить скрипт на ТВ
файлом (`scp tv_restart.sh`) и вызвать `sh /tmp/tv_restart.sh`:
```sh
#!/bin/sh
luna-send -n 1 -f luna://com.webos.applicationManager/close '{"id":"com.lampac.tv"}'
sleep 5
luna-send -n 1 -f luna://com.webos.applicationManager/launch '{"id":"com.lampac.tv"}'
```
🔴 **`eval()` не делает функции глобальными.** Инжект `eval(code)` внутри IIFE плагина
оставляет `wlHost`/`wlAbs` в замыкании → снаружи (`wlHost()`) `ReferenceError`.
Это нормально: `cub.main` их видит, внешние пробы — нет. Проверять только через
вызов `Lampa.Api.main()` или чтение DOM.
### Перезапуск Lampa на ТВ — обязателен
🔴 **`docker restart lampac` НЕ перезагружает плагин в памяти ТВ.** `catalog.js`
закэширован браузером webOS. Признак «ТВ видит старую версию»:
`String(Lampa.Api.sources.cub.main)` содержит старый код.
Порядок: залить `plugin.js` на Кра́кен → `docker restart lampac` (сброс `Staticache`) →
**перезапустить Lampa на ТВ** (`tv_restart.sh`) → `probe_ui.py`.
## 🔴 КАРТОЧКА: механика card_parse (разобрано по коду 2026-09-22)
Клик по элементу каталога ведёт в `/catalog/card?plugin=watchlist&uri=<id>&type=movie`.
Без `card_parse` Catalog строит URL как `$"{init.host}/{uri}"` → `http://127.0.0.1:9118/movie/424781`
— такого пути нет, **HTTP 500**, в UI «здесь пусто».
**Что даёт `card_parse` (по `CardController.cs`):**
| Поле | Механика | Подстановки |
|---|---|---|
| `initUrl` | исполняется **C#-выражением** через `CSharpEval`, объект `CatalogInitUrlCard(host, args, uri, query, type)` | ✅ **есть `uri`, `type`, `host`** |
| `node` | `json.SelectToken(parse.node)` — **чистый JSONPath** | ❌ **нет**, статичен |
| `name` / `image` / `description` | `ModInit.nodeValue(...)` → `attribute` | — |
**Сигнатура роута (критично):**
```csharp
[Route("catalog/card")]
public async Task<ActionResult> Index(string plugin, string uri, string type)
```
🔴 **Параметра `cat` НЕТ.** Поэтому `initUrl: $"…/cards-{cat}.json"` даёт `cards-.json` → 404
→ `html == null` → `BadRequest("html")` → HTTP 500. Карточка **не знает категорию** — она
получает только `uri` и `type`.
**Питфолл JSONPath:** ключ `movie/424781` в `SelectToken` трактуется как **вложенность**
(`movie` → `424781`) → узел не найден → **HTTP 400 `parse.node`**. Слэш в JSONPath = разделитель.
**Итоговое решение (в YAML уже записано, деплой не доделан):**
```yaml
card_parse:
initUrl: |
return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
node: card
name: { attribute: name }
original_name: { attribute: original }
year: { attribute: year }
image: { attribute: poster }
description: { attribute: overview }
```
Файлы `wwwroot/cards/card-<media>-<id>.json` (345 шт., ~1 КБ) с обёрткой `{"card": {...}}`
и фиксированным путём `$.card` — потому что `node` статичен и один общий файл по `uri`
не отфильтровать.
**Диагностика карточки (воспроизводимо):**
```bash
curl -s -o /tmp/c.json -w "HTTP %{http_code}\n" \
"http://192.168.1.14:9118/catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie"
# 500 (нет card_parse) / 400 parse.node (node не нашёл узел) / 200 + JObject (ок)
```
⚠️ Ошибки карточки **в логи не пишутся** (`docker logs lampac` пуст) — диагностировать только
по HTTP-коду и телу ответа (`parse.node`, `html`, `json`).
## 🔴 ПИТФОЛЛ ДЕПЛОЯ: sudo через ssh без tty
`ssh kraken "sudo docker cp …"` падает с `sudo: a terminal is required to read the password`.
`-t` обязателен, но при этом **нельзя передавать `a && b`** — цепочка рвёт tty-сессию
(«Connection to kraken closed»). Каждую команду вызывать **отдельно**:
```python
full = f"ssh -t {KRAKEN} {cmd}" # одна команда, без &&
```
⚠️ **`docker compose up -d` НЕ пересоздаёт контейнер, если менялся только список volumes.**
Симптом: `docker inspect` не показывает новое монтирование, патч не виден.
**Фикс: `docker compose up -d --force-recreate`.** После — проверить
`docker inspect lampac --format '{{json .Mounts}}' | jq -r '.[].Destination'`.
## ✅ ДЕПЛОЙ: что залито и чем (2026-09-22, третья сессия)
**Скрипты (на Mac, `/Users/admin/lampac-tmp/`):**
| Файл | Роль |
|---|---|
| `gen_watchlist_v2.py` | генератор: `.md` → постеры/TMDB-ID/карточки (Radarr → TMDB → Кинопоиск-фолбэк) |
| `patch_plugin.py` | патч `plugin.js`: `+enumerable:true,configurable:true` в оба `defineProperty` |
| `plugin-patched.js` | результат патча (10738 Б), `node --check` чист |
| `deploy_lampac.py` | заливка JSON/YAML/plugin.js + volume + пересоздание + самопроверка |
| `watchlist.yaml` | описание каталога для Catalog (4 категории + `card_parse`) |
| `docker-compose.yml` | + volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` |
| `movies-watchlist.md` | локальная копия списка (682 строки) для прогонов |
**Кэши генератора (в `$HOME`):**
- `~/.lampac_tmdb_cache.json` — `{title|year: [tmdb_id, media_type, poster_path]}` (поиск)
- `~/.lampac_tmdb_details.json` — `{movie/ID: {overview, genres, cast, runtime, vote, …}}` (детали карточки)
**Что даёт прогон (замер):**
```
films: 360 постеров 308 TMDB-ID 312
new: 33 постеров 30 TMDB-ID 30
cartoons 3 постеров 3 TMDB-ID 3
anime: 7 постеров 7 TMDB-ID 7
источники: Radarr 9, TMDB 343, Кинопоиск-фолбэк 51, карточек 352
```
**Артефакты в `wwwroot/` (все на HDD Кра́кена):**
`watchlist.json` · `watchlist-{films,new,cartoons,anime}.json` · `cards.json` · `cards/card-<key>.json`
**Команды (порядок):**
```bash
cd /Users/admin/lampac-tmp
python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
python3 deploy_lampac.py /tmp/wl-out
```
⚠️ После правки генератора **перезапускать его**, а не только деплой — иначе в `wwwroot`
уедет старый формат файлов (один раз на этом потерял `cards.json` как список вместо словаря).
## 🔴 АРХИТЕКТУРНАЯ ОШИБКА 2026-09-22 — почему каталог «не работает»
**Что хотел Alex (дословно):**
> «Я предполагал что будет отображаться то что и раньше (список cub) но В НЕГО будет
> сверху добавлено несколько секций в которых будут РЕАЛЬНЫЕ ЭЛЕМЕНТЫ — а не сгенерируют
> пустые заглушки! Нахуя это надо то?! Чё блядь значит оно так и должно быть?!»
**Что сделано вместо этого — две независимые ошибки:**
> ⚠️ **ОПРОВЕРГНУТО ТЕСТОМ 2026-09-22 (поздний вечер).** Раздел ниже описывает
> **неверную гипотезу** («`menu` подменяет / `main` добавляет»). Проверка на живом
> сервере показала: **блока `main` в YAML не существует** — он ломает парсинг.
> Подмену разделов делает **`plugin.js`**, а не YAML. Читать раздел
> **«🔴 ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН»** ниже — там правильная механика.
### Ошибка 2 — источник данных: парсинг `.md` вместо Radarr API
Генератор читал `movies-watchlist.md` — **текстовый список названий**. В нём
**нет ни TMDB ID, ни постеров**. Результат:
| Метрика | Значение |
|---|---|
| Всего позиций (films) | 360 |
| Из них с постером (`img != null`) | **0** |
| Из них с реальным ID (`film/...`) | **18** |
Остальные 342 отдавались с `id` = **ссылка на поиск Кинопоиска**
(`index.php?kp_query=...`). Отсюда жалоба «при открытии элемента пишет *здесь пусто*»:
Lampa идёт по ссылке, получает страницу поиска, а не фильм.
**Замер 2026-09-22 (доказательство, воспроизводимо):**
```bash
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films" \
| jq '[.results[] | select(.img == null)] | length' # → 360
curl -s "..." | jq '.results | length' # → 360
curl -s "..." | jq '[.results[] | select(.id | startswith("film/"))] | length' # → 18
```
То есть **постеров 0 из 360**, рабочих карточек 18 из 360.
**Почему «девять» с маленькой буквы и `Gods don't give gifts`** — это не баг каталога,
а буквально строки файла (проверено `grep -n`):
```
56: - [ ] Gods don't give gifts
64: - [ ] ❓ девять
```
Каталог отдаёт строки как есть. Обе — в секции `## Новинки` (отступ 2 пробела, подсписок).
Alex: «Отображаются элементы типа "девять"(с маленькой буквы), Gods don't give gifts —
который вообще ещё не вышел — короче не реальные фильмы а буквально пункты списка.»
**Правильный источник — Radarr API.** Alex: «Они уже резолвятся же блядь!! Оно же
добавлялось в arr!» — TMDB ID и постеры уже есть там, потому что фильмы добавлялись
в Radarr.
```bash
# API-ключ Radarr
grep -oE "<ApiKey>[^<]+</ApiKey>" /srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config.xml
# → cbcb8ec320104359920c0f611d503c7b
# ⚠️ ВАЖНО: -L обязателен! Без него Radarr отдаёт 307, тело пустое
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length
# → 50
```
**Форма ответа Radarr (то, что нужно было брать с самого начала):**
```json
{
"tmdbId": 1292695,
"title": "They Will Kill You",
"year": 2026,
"hasFile": true,
"images": [{"coverType":"poster",
"remoteUrl":"https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28R"}]
}
```
Есть **всё**: TMDB ID → рабочий `href`, `remoteUrl` → постер, `title`/`year` → название,
`hasFile` → скачан или нет. Плюс постеры локально: `/config/MediaCover/` (46 каталогов).
⚠️ **Покрытие:** в Radarr **50 фильмов**, в `.md` — **360**. Разница (310) — то, что
в список записано, но в Radarr не заказано. Что с ними делать — **решение Alex
не получено**, открытый вопрос №1 ниже.
### Что НЕ надо было делать (записано, чтобы не повторять)
- Парсить текст вместо API, где данные уже резолвлены
- Складывать `menu` + `main` одновременно
- Считать «список названий» готовым каталогом для UI с постерами
### Alex: корневая претензия к процессу
> «А нахуя ты это так реализовал?? Подумать блядь вообще не твое!»
Разбор: задача звучала как «покажи список в UI» — и была прочитана буквально
(«отдать строки файла»). **Надо было сначала выяснить, откуда брать постеры и ID**
(а ответ — Radarr, куда фильмы и добавлялись), а не начинать с парсера текста.
## Задача
Показывать список фильмов Alex (`family/documents/movies-watchlist.md`) **в UI Lampa на ТВ** — каталогом.
Не локальный плейбэк. Формулировка Alex: «отображение фильмов из списка».
**Уточнение от 2026-09-22 (важное!):** каталог **добавляется СЕКЦИЯМИ СВЕРХУ** к родному
каталогу Lampa. Родной CUB-контент **не подменяется и не исчезает**. Элементы — **реальные**
фильмы с постерами и карточками, а не пустые заглушки (см. «🔴 АРХИТЕКТУРНАЯ ОШИБКА»).
⚠️ **Что НЕ спрашивать повторно.** Задача — **отображение**, не воспроизведение.
Список — это перечень, а не источник видео. Вопрос «а что играть по клику?» —
**выдумка сверх постановки**, Alex на него реагировал раздражением («я чё тебе сказал в постановке задачи»).
Штатное воспроизведение Lampa (торренты/онлайн) работает само, отдельно решать нечего.
## ✅ Затык снят: чем это делается
В образе Lampac уже есть модуль **`Catalog`** — YAML-каталоги → плагин Lampa `/catalog.js`.
Сейчас **выключен**. Писать плагин с нуля **не нужно**.
## Как работает Catalog (разобрано по коду 2026-09-22)
**Файлы в контейнере:** `/lampac/module/Catalog/` — `Controllers/`, `plugin.js`, `README.md`,
`sites/*.yaml` (10 готовых примеров: `vokino`, `rezka`, `kinopub`, `anime`, …).
### Механика
| Что | Как |
|---|---|
| Данные | `sites/<site>.yaml` — описание сайта: `host`, `menu`, `search`, `list`, `content` |
| Подстановки | `override/<site>.yaml` и `override/_.yaml` — переопределения поверх базового |
| Роуты | `/catalog.js` (плагин), `/catalog` (JSON каналов), `/catalog/list`, `/catalog/card` |
| WAF | `^/catalog/` → **10 запросов/сек** |
| Имя файла | только `[a-z0-9-]`; есть fallback-поиск по `displayname` |
### Ключевое: обязателен HTTP
```csharp
if (!url.StartsWith("http"))
url = $"{init.host}/{url}"; // ListController.cs:87
```
`host` из YAML сканируется **по HTTP** (`Http.Get` / Playwright / RCH).
**Локальные файлы напрямую Catalog читать не умеет** — только HTTP-ответ.
### Два режима парсинга
```csharp
bool? jsonPath = contentParse.jsonPath ?? init.jsonPath;
if (jsonPath == true) json = JToken.Parse(html); // JSON-режим
else doc.LoadHtml(html); // HTML-режим
```
- `jsonPath: true` → ответ парсится как **JSON**, элементы берутся по `nodes` (`$.channels[*].details`)
- иначе → HTML через HtmlAgilityPack
🔴 **Это и есть рычаг:** любой **свой HTTP-эндпоинт, отдающий JSON нужной формы**, подойдёт как `host`.
Тогда задача сводится к генерации JSON + YAML-обёртки, без правки кода Lampac.
### Форма элемента (`content` в YAML)
```yaml
content:
nodes: "$.channels[*].details" # jsonPath-выражение (JSON) или CSS/XPath (HTML)
name: { attribute: name }
original_name:{ attribute: originalname }
href: { attribute: ident }
image: { attribute: poster }
year: { attribute: released }
serial_key: { attribute: type, format: | return value is "serial" ? value : null; }
args:
- { name_arg: vote_average, attribute: rating_kp }
```
Карточка — отдельная секция `card_parse` + `card_args` (описание, рейтинги, жанры, страна).
Полный живой пример — `sites/vokino.yaml` (копия в [[personal/tech/lampac-deployment]], раздел Catalog).
## Включение модуля — 2 правки
```jsonc
// current.conf (внутри контейнера /lampac/current.conf)
"SkipModules": [ "Tracks", "Transcoding", ... ] // ← убрать "Catalog" (строка ~277)
"plugins": { ..., "catalog": true } // ← было false (строка ~1653)
```
🔴 **ПИТФОЛЛ — `init.conf` НЕ смонтирован.** В нашем `docker-compose.yml` нет volume на `/lampac/init.conf`,
файл живёт в образе. Правки через `docker exec` **исчезнут при пересоздании контейнера**
(`docker compose up`, watchtower-обновление образа). Варианты:
- (а) добавить volume `./config/init.conf:/lampac/init.conf:ro` — надёжно, но нужен первичный `docker cp`
- (б) править через **AdminPanel** `/adminpanel` (модуль, правит `init.conf` + `current.conf`)
- (в) `docker cp` + commit — плохо
**Рекомендация: (а)** — конфиг на HDD, переживает пересоздание.
## Архитектура решения
```
family/documents/movies-watchlist.md ← источник (vault, git-синхрон)
│ существующий парсер: watchlist-sync/parser.py
▼
генератор (Python, на Кра́кене)
│ пишет JSON нужной формы
▼
/lampac/... JSON-эндпоинт, доступный по HTTP с Lampac
│
▼
sites/watchlist.yaml ← Catalog-описание, host=<наш JSON URL>
menu: Фильмы / Мультфильмы / Аниме (по секциям .md)
│
▼
Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js
```
**Откуда отдавать JSON — варианты (выбрать на этапе реализации):**
| # | Где | Плюс | Минус |
|---|---|---|---|
| 1 | **статика в `/lampac/wwwroot/`** | ничего не поднимать, Lampac сам отдаёт | только чтение, обновление = перезапись файла |
| 2 | **отдельный лёгкий HTTP-сервер** на Кра́кене | есть API/логика | ещё один контейнер |
| 3 | `file://` | — | ❌ не поддерживается (только http) |
**Склоняюсь к (1)** — генерировать `wwwroot/watchlist.json`, Lampac отдаёт его сам, без новых сущностей.
## Обновление каталога — реализовано
Скрипт `~/scripts/watchlist-catalog.sh` (root:root, ставить только `sudo install -m 755`):
1. `python3 $LAMPAC/scripts/gen_watchlist.py <watchlist.md> $LAMPAC/wwwroot/watchlist.json`
2. `docker restart lampac` — **обязательно**, сбрасывает кэш Catalog (см. питфолл 3)
Cron: `30 2 * * *` → лог `~/logs/watchlist-catalog.log`.
Время выбрано после `watchlist-nightly` (01:00) и `watchlist-cleanup` (02:00).
Бэкап crontab перед установкой: `~/crontab-backup-20260922-112915.txt`.
⚠️ **`watchlist-catalog.sh` сделан отдельной задачей, а не врезан в `watchlist-nightly.sh`.**
Причина: `nightly` — рабочий боевой скрипт, его падение остановило бы весь пайплайн списка.
Отдельный cron падает изолированно.
## Что осталось — ПЕРЕДЕЛКА (после признания архитектуры неверной)
| # | Шаг | Статус |
|---|---|---|
| 1 | Генератор берёт данные **из Radarr API** (`localhost:7878/api/v3/movie`), а не парсит текст | ✅ сделано (9 из Radarr) |
| 2 | Подставлять в `id` → `tmdbId`, в `img` → `images[].remoteUrl` (постер), `year` → `year` | ⏳ |
| 3 | ~~Из `sites/watchlist.yaml` убрать блок `menu`, оставить `main`~~ ❌ **НЕВОЗМОЖНО** — `main` в YAML ломает парсинг, `menu` обязателен. Подмену делает `plugin.js`. См. «🔴 ПИТФОЛЛ YAML» | ❌ опровергнуто |
| 4 | Постеры кэшировать локально (Radarr `MediaCover/` или проксировать через `tmdbproxy`) | ⏳ |
| 5 | Фикс `plugin.js` (`enumerable:true`) — **отдельная независимая правка**, см. ниже | ⏳ ждёт апрува |
**Порядок:** шаги 1–4 закрывают жалобу «пустые заглушки + исчез CUB».
Шаг 5 — про то, что источника вообще не видно в списке выбора.
Приёмка (формулировка Alex): **родной список CUB на месте, сверху добавлены ряды
с реальными фильмами из списка, у элементов есть постеры и рабочие карточки.**
## Диагностический скрипт для ТВ (`/tmp/lampa_probe.py`)
Правки в UI Lampa по SSH напрямую не сделать, но **состояние читается** через DevTools:
```python
pages = json.load(urllib.request.urlopen("http://192.168.1.70:9998/json", timeout=8))
target = next(p for p in pages if "com.lampac.tv" in p.get("url",""))
ws = websocket.create_connection(target["webSocketDebuggerUrl"], timeout=10)
# Runtime.evaluate:
# Lampa.Storage.field('source')
# Object.keys(Lampa.Api.sources).join('|')
# Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список')
# Lampa.Api.sources['Мой список'].getCatalog()
```
Требует `pip3 install websocket-client` (стоит). См. [[family/how-to/tv-lampac-plugin]].
## Известные ограничения
| Ограничение | Детали |
|---|---|
| **Постеров нет** | `image` пустой. Опционально — тянуть через TMDB-кэш Lampac (`tmdbproxy`) |
| **Карточки нет** | Только список названий. `card_parse` не настроен — Lampa может показывать пустую карточку |
| **`🗓️` upcoming показываются** | Не прячем. Решение Alex не запрашивалось |
| **Перезапуск контейнера** | Каждое обновление каталога = `docker restart lampac` (~30 сек простоя). Кэш не инвалидируется иначе |
## ⚠️ Что проверено, а что нет
**Проверено по HTTP (серверная часть):**
| Проверка | Результат |
|---|---|
| `/catalog.js` по IP | 200, ~32 КБ, `api_host:'http://192.168.1.14:9118'` |
| Каталог в `/catalog` | **7 ключей**, «Мой список» первым (Rezka, Kinobase, FilmixApp, VoKino, Eneyida, KinoUkr, Мой список) |
| JSON-файлы | `watchlist.json` + `watchlist-{films,new,cartoons,anime}.json` — все 200 |
| Выдача по категориям | films 360 · new **33** · cartoons 3 · anime 7 (после фикса сирот 2026-09-22) |
| Формат ответа `/catalog/list` | `{page, results, total_pages}`, `total_pages: 1` для films |
| Сортировка | ⭐ → рейтинг ↓ → ужасы |
| Чистота названий | эмодзи-маркеров 0 |
| Сирота «Конец света» | ❌ отсутствует во всех 4 категориях |
**НЕ проверено:**
- ✅ **UI Lampa на ТВ — ПРИЧИНА НАЙДЕНА 2026-09-22** (см. ниже «🔴 ГЛАВНЫЙ ПИТФОЛЛ»).
Диагностика через Chrome DevTools ТВ (`http://192.168.1.70:9998/json`):
- ТВ **физически достаёт** Кра́кен — `curl` с ТВ отдаёт `/catalog` 200 за 0.26 с.
- Lampa **работает и ходит на сервер** — соединение `192.168.1.70 → 192.168.1.14:9118` ESTABLISHED.
- `Storage.field('source')` = **`'Мой список'`** — источник уже выбран!
- `Lampa.Api.sources['Мой список']` **существует** и отдаёт каталог
(`getCatalog()` → `search, search_lazy, movie, cartoons, anime, menu, main`).
- ❌ НО: `Object.keys(Lampa.Api.sources)` = `'tmdb|cub'` — **источника нет в перечислении**.
- Причина: `defineProperty(..., {get: ..., enumerable:false, configurable:false})`.
- **Диагноз: баг в `plugin.js` самого Lampac, а не в нашей настройке.** UI Lampa строит
список источников через `Object.keys()` — «Мой список» из него выпадает, поэтому в UI
источник не видно и выбрать его нельзя.
- ❌ Боевой прогон крона в 02:30 (скрипт отработал вручную успешно).
- ❌ Карточка/постеры в UI.
## 🔴 ГЛАВНЫЙ ПИТФОЛЛ: источник не виден в UI Lampa (`enumerable: false`)
**Симптом.** Alex: «Ни на главной ни в Фильмах нет! Там только Сейчас смотрят, рекомендуем итд».
Плагин добавлен, серверная часть отдаёт всё корректно, а в UI источника нет.
**Диагностика (по возрастанию точности):**
1. `ssh kraken 'sudo ss -tn | grep ":9118"'` — сначала показал **0 соединений** (ложный вывод:
замер сделан в момент старта Lampa). Через 30 с на ТВ:
`netstat -tn | grep 9118` → **1 ESTABLISHED + 6 TIME_WAIT** — Lampa скачала все файлы.
2. Foreground-приложение: `luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}`
→ `com.lampac.tv`. Запускать надо `launch` с **экранированным JSON в кавычках**,
иначе `errorCode: 2 invalid parameters`.
3. Доступ к отладчику ТВ: **`curl http://192.168.1.70:9998/json`** — webOS отдаёт Chrome
DevTools Protocol на порту 9998 (у `WebAppMgr` в аргументах `--remote-debugging-port=9998`).
Через `websocket` + `Runtime.evaluate` читаются переменные живой Lampa.
4. Ключевой замер: `JSON.stringify(Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список'))`
→ `{"enumerable":false,"configurable":false}`.
**Корень.** В `/lampac/module/Catalog/plugin.js` (строка 59, `loadCatalogs()`, **два вхождения**):
```js
Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; } });
```
Свойство создаётся не-enumerable → `Object.keys()` его не видит → Lampa не рисует источник
в панели выбора. При этом `Api.sources['Мой список']` доступен напрямую и каталог отдаёт.
**Фикс** (одна правка, два вхождения):
```js
Object.defineProperty(Lampa.Api.sources, key,
{ get: function(){ return Api; }, enumerable: true, configurable: true });
```
**Как применять — `plugin.js` живёт в образе, не на HDD!** (проверено:
`docker inspect` не показывает монтирования на `module/Catalog`, монтируются только
`catalog/sites` и `catalog/override`). Правка через `docker exec` **исчезнет при обновлении образа**.
Предложенный путь (на апруве у Alex, 2026-09-22):
```yaml
# docker-compose.yml
volumes:
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
```
```bash
# 1. забрать, поправить локально (sed/grep по файлу не применять — только patch)
ssh kraken 'docker exec lampac cat /lampac/module/Catalog/plugin.js' > /tmp/catalog-plugin.js
# 2. бэкап
cp /tmp/catalog-plugin.js /tmp/catalog-plugin.js.bak-$(date +%Y%m%d-%H%M%S)
# 3. правка двух вхождений defineProperty → +enumerable:true,configurable:true
# 4. проверка синтаксиса ПЕРЕД заливкой
node --check /tmp/catalog-plugin.js
# 5. на HDD, добавить volume, пересоздать
scp /tmp/catalog-plugin.js kraken:/srv/.../lampac/catalog/plugin.js
ssh kraken 'cd /srv/.../lampac && docker compose up -d'
# 6. проверка на ТВ: Object.keys(Lampa.Api.sources) должен стать 'tmdb|cub|Мой список'
```
**Откат:** убрать строку volume из compose → `docker compose up -d`. Плюс бэкап файла в `/tmp`.
**Статус: правка НЕ применена** — ждёт апрува Alex (2026-09-22).
## Готовые команды диагностики ТВ (переиспользовать)
```bash
# 1. Что запущено на ТВ (luna-send пишет в tty → нужен script -q -c)
ssh root@192.168.1.70 'script -q -c "luna-send -n 1 -f \
luna://com.webos.applicationManager/getForegroundAppInfo {}" /dev/null'
# 2. Запустить приложение (JSON экранирован И обёрнут в кавычки)
ssh root@192.168.1.70 'script -q -c "luna-send -n 1 -f \
luna://com.webos.applicationManager/launch \"{\\\"id\\\":\\\"com.lampac.tv\\\"}\"" /dev/null'
# 3. Достаёт ли ТВ Кра́кен
ssh root@192.168.1.70 'curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
--max-time 8 http://192.168.1.14:9118/catalog'
# 4. Живые соединения с Lampac (на ТВ, не на сервере!)
ssh root@192.168.1.70 'netstat -tn 2>/dev/null | grep 9118'
# 5. DevTools-цель Lampa на ТВ
curl -s --max-time 8 "http://192.168.1.70:9998/json" | /usr/bin/jq -r '.[] | "\(.type): \(.url)"'
```
Скрипты-пробы DevTools: `/tmp/lampa_probe.py` (состояние источника),
`/tmp/lampa_probe3.py` (асинхронный вызов `menu()` каталога).
⚠️ `tcpdump` на Кра́кене **нет** — для съёма трафика ставить не стали, использован
DevTools-подход (дал больше: видно состояние JS, а не только пакеты).
⚠️ `luna://com.webos.service.capture/takeScreenShot` на этом ТВ **не существует**
(`Unknown method`) — скриншот так не снять.
## Файлы реализации (на HDD Кра́кена)
Корень: `/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/`
| Путь | Роль |
|---|---|
| `config-module/init.conf` | включает Catalog → `/lampac/init.conf:ro` |
| `catalog/sites/watchlist.yaml` | описание каталога |
| `catalog/override/` | переопределения (пусто) |
| `wwwroot/watchlist.json` | все секции |
| `wwwroot/watchlist-<code>.json` | по файлу на категорию |
| `scripts/gen_watchlist.py` | генератор |
| `docker-compose.yml` | 7 монтирований; бэкапы `.bak-*` |
На хосте: `~/scripts/watchlist-catalog.sh`, лог `~/logs/watchlist-catalog.log`.
## Откат
```bash
# выключить каталог
ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \
/srv/.../lampac/config-module/init.conf && docker restart lampac'
# в Lampa убрать плагин catalog.js
# cron: crontab -l | grep -v watchlist-catalog | crontab -
```
Ничего в коде Lampac не патчилось — только конфиг и данные.
## Статус: ✅ РЕАЛИЗОВАНО 2026-09-22
Каталог «Мой список» работает, отдаётся в Lampa через `/catalog.js`.
### Что сделано
| # | Шаг | Результат |
|---|---|---|
| 1 | Включён модуль Catalog | `SkipModules` без Catalog + `catalog: true` → `/catalog.js` 200 |
| 2 | `init.conf` смонтирован | `./config-module/init.conf:/lampac/init.conf:ro` — правится на HDD, не теряется |
| 3 | `sites/` + `override/` смонтированы | `./catalog/sites`, `./catalog/override` |
| 4 | `wwwroot` смонтирован | `./wwwroot` — Lampac сам отдаёт JSON по HTTP |
| 5 | Генератор написан | `lampac/scripts/gen_watchlist.py` |
| 6 | `sites/watchlist.yaml` создан | `lampac/catalog/sites/watchlist.yaml` |
| 7 | Автообновление в cron | `30 2 * * *` → `~/scripts/watchlist-catalog.sh` |
| 8 | Проверка | 4 категории: films 360, new 34, cartoons 3, anime 7 |
### Архитектура (финальная)
```
family/documents/movies-watchlist.md ← источник
│ gen_watchlist.py (свой парсер, ~200 строк)
▼
lampac/wwwroot/watchlist.json ← все секции
lampac/wwwroot/watchlist-<code>.json ← по файлу на категорию
│ Lampac отдаёт wwwroot по HTTP сам
▼
lampac/catalog/sites/watchlist.yaml ← описание для Catalog
│ plugin=watchlist, jsonPath: true, nodes: $.items[*]
▼
Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js
```
### 🔴 Питфоллы, найденные по ходу (не повторять)
1. **Catalog ВЫБРАСЫВАЕТ элемент с пустым `href`.**
`ListController.cs`: `if (!IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href))`.
Симптом: из 404 позиций отдалось 52 — ровно те, у кого есть ссылка Кинопоиска.
**Фикс:** генератор подставляет fallback-href
`https://www.kinopoisk.ru/index.php?kp_query=<название>`.
2. **Catalog НЕ подставляет `{cat}` в `nodes`.**
JSONPath статичен, отфильтровать секцию внутри одного файла нечем.
Симптом: все категории отдавали весь список (404 вместо 360/34/3/7).
**Фикс:** генератор пишет отдельный плоский файл на категорию
(`watchlist-films.json` и т.д.), `nodes: "$.items[*]"` одинаков для всех.
3. **Кэш Catalog без версии данных.**
Ключ `catalog:{plugin}:{search}:{sort}:{cat}:{page}` — не зависит от содержимого.
Симптом: после перегенерации JSON выдача не менялась.
**Фикс:** `docker restart lampac` в скрипте обновления.
4. **`init.conf` не был смонтирован.**
Файл живёт в образе; правки через `docker exec` теряются при пересоздании.
Правки шли в `current.conf` (сгенерированный) — неверная цель.
**Фикс:** volume `:ro` + свой минимальный `init.conf` (перекрывает `base.conf`).
5. **`api_host` подставляется из запроса** (`{localhost}` → хост, по которому пришли).
По IP `192.168.1.14` отдаёт правильный адрес — специально ничего настраивать не нужно.
6. **Кириллица в inline-jq внутри `ssh '...'`** ломает команду (tirith + quoting).
Выносить в файл или убирать строки с кириллицей.
7. **Осиротевшие строки в `## Новинки` давали дубли в каталоге.**
Строки без `👍`/`👎` (например `- [ ] ⬇️ Название`) `process-thumbs` не трогал —
они висели в Новинках навсегда. Если фильм уже `[x]` в главном списке — получался дубль:
непросмотренный «Конец света» при просмотренном оригинале.
**Фикс в `watchlist-sync/parser.py`** (удаление сирот + флаг `_patch_dropped`) и `cli.py`.
Разбор: [[family/projects/watchlist-automation]], секция «ОСИРОТЕВШИЕ СТРОКИ».
8. **Двойное экранирование при правке `parser.py` скриптами.**
`r'\1'` превращался в байт `\x01`, `r'\b'` — в `\x08`. Синтаксис валиден, регекспы молчат.
**Проверять файл на `chr(1)` и `chr(8)` после патча.**
### Управление
```bash
ssh kraken
cd /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac
# перегенерировать каталог + сбросить кэш
bash ~/scripts/watchlist-catalog.sh
# логи
tail -f ~/logs/watchlist-catalog.log
# только генератор (без перезапуска)
python3 scripts/gen_watchlist.py \
/home/kraken/obsidian/family/documents/movies-watchlist.md \
wwwroot/watchlist.json
```
### Откат
```bash
# выключить каталог
ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \
/srv/.../lampac/config-module/init.conf && docker restart lampac'
# в Lampa убрать плагин catalog.js
```
## Правила отбора и сортировки (утверждено Alex 2026-09-22)
### Фильтрация
| Правило | Действие |
|---|---|
| `[ ]` непросмотренные | **показывать** |
| `[x]` просмотренные | ❌ скрыть |
| `👎` | ❌ скрыть |
| `🗓️` upcoming | показывать (решить при реализации — есть в файле) |
| `❓` ambiguous | показывать (название как есть) |
### Сортировка внутри списка — трёхключевая
1. **`⭐` (U+2B50) — первыми**
2. **Рейтинг — по убыванию** (IMDb/KP из строки файла; строки `[название](kp) (год) IMDb N.N KP N.N`)
3. **Ужасы (`🧟♀️`, TMDB genre_id=27) — последними**
⚠️ **Только `⭐` (U+2B50)** — как в `sync-down` (см. [[family/projects/watchlist-automation]]).
`✭` (U+272D) приоритетом **не является**.
### Порядок секций
1. **Главный список — первым.** Это `## Фильмы`
2. Дальше — **остальные по порядку в файле**: `## Новинки - 👍/👎`, `## Мультфильмы`, `## Аниме`, …
⚠️ `## Новинки👎` — **не показывать** (это скрытая секция отказов).
⚠️ Секция `## Новинки` содержит `👍`/`👎`; `👎`-строки прячем, `👍` показываем.
### Источник рейтинга
В строках со ссылками Кинопоиска рейтинг **уже в тексте**: `IMDb 7.6 KP 8.2`.
Для строк без рейтинга (например `- [ ] Gods don't give gifts`) — сортировать как `0`, в конец группы.
Опционально: добирать через TMDB (кэш `tmdbproxy` в Lampac) — решить при реализации.
## 🟢 РЕШЕНИЕ: первая категория игнорирует `format` (2026-09-22, вторая сессия)
**Это была настоящая причина «пустого каталога».** Не `main`, не кэш, не монтирование.
### Симптом
`cat=films` отдавал **360 элементов** с `img: null` и `id` = `index.php?kp_query=...`
(ссылка на поиск Кинопоиска). При этом `watchlist-films.json` на диске содержал
совсем другое. Клик по элементу → Lampa идёт на страницу поиска → «здесь пусто».
### Диагностика (воспроизводимая)
Подменил `watchlist-films.json` пробником из 1 элемента («Project Hail Mary»
с реальным TMDB ID и постером). Результат запросов:
| Запрос | Ответ | Что это значит |
|---|---|---|
| `cat=films` | **360** (старые данные) | файл НЕ прочитан |
| `cat=films&sort=rating` | **1** ✅ | файл прочитан |
| `cat=new` | 33 ✅ | файл прочитан |
| `cat=cartoons` | 3 ✅ | файл прочитан |
| `cat=anime` | 7 ✅ | файл прочитан |
Пробник был 343 байта, HTTP внутри контейнера отдавал 343 байта — **но Catalog
возвращал 360**. Значит Catalog читал **другой URL**.
### Корень — `ListController.cs:50–87`
```csharp
string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
? init.list?.firstpage : init.list?.uri)}"; // ← строка 50
...
else if (!string.IsNullOrEmpty(cat))
{
var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat));
string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-");
if (!string.IsNullOrEmpty(eval)) { /* url = CSharpEval(...) */ } // ← строка 78–85
if (!url.StartsWith("http")) url = $"{init.host}/{url}"; // ← строка 87
}
```
Блок `format` применяется **только в ветке `else if (!string.IsNullOrEmpty(cat))`** —
то есть **не для реально первой** категории в переборе. Эмпирически: категория,
стоящая **первой** в `menu.categories`, берёт **базовый `url` из `list.firstpage`**,
и `format` для неё не срабатывает.
У меня первой стояла `Фильмы` → она читала `list.uri`/`firstpage`, а он в тот момент
указывал на общий `watchlist-films.json`, содержимое которого подтягивалось из
`watchlist.json` (все каналы, 360 позиций films).
### Фикс
**Поставить первой в `menu.categories` ту категорию, файл которой и лежит в `list.uri`.**
В рабочем `watchlist.yaml`:
```yaml
menu:
- categories:
Новинки: new # ← ПЕРВОЙ (читает list.uri = watchlist-new.json)
Фильмы: films # ← через format
Мультфильмы: cartoons
Аниме: anime
format:
"-": |
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
list:
uri: watchlist-new.json # ← согласовано с первой категорией
firstpage: watchlist-new.json
total_pages: 1
```
⚠️ **Правило на будущее:** `list.uri` и первая категория в `menu.categories`
должны указывать на **один и тот же файл**. Если переставить категории местами —
поправить `list.uri`, иначе первая категория снова отдаст чужие данные.
Этот комментарий вписан прямо в `watchlist.yaml` на HDD.
### Что ещё опробовано и НЕ сработало (не повторять)
- `?nocache={Guid.NewGuid()}` в `format` — не помогло, дело не в кэше
- Перезапуск контейнера — не помогло (кэш ни при чём, `hybridCache` в памяти процесса)
- Разбивка на 4 отдельных `menu`-записи с разными `format` — **сломало всё**, канал отдавал пустоту
- Явный `format["sort"]` — тоже путь в никуда, `sort` у меня не используется
### Артефакты, написанные в этой сессии (локально, `/Users/admin/lampac-tmp/`)
| Файл | Роль | Статус |
|---|---|---|
| `gen_watchlist_v2.py` | генератор с постерами: Radarr → TMDB → фолбэк Кинопоиск | ✅ прогнан, 330 постеров из 403 |
| `patch_plugin.py` | патч `plugin.js` (+`enumerable:true` в 2 местах) | ✅ `node --check` чист |
| `plugin-patched.js` | результат патча (10738 Б) | ✅ готов |
| `watchlist.yaml` | исправленный порядок категорий + комментарий-питфолл | ✅ готов |
| `docker-compose.yml` | + volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` | ✅ готов |
| `deploy_lampac.py` | заливка 7 шагов: бэкап → JSON → YAML → plugin → volume → restart → проверка | ✅ готов, не запущен |
| `movies-watchlist.md` | локальная копия источника (682 строки) | ✅ |
**Генерация занимает ~8–10 минут** (403 фильма × запросы к TMDB, `time.sleep(0.02)`).
Кэш: `~/.lampac_tmdb_cache.json`. Прогресс — в stderr, каждые 25 позиций.
🔴 **Запускать в background** (`notify_on_complete=true`) — foreground упрётся в 600 с.
### Питфоллы `gen_watchlist_v2.py` (найдены в работе)
1. **Хвостовой год в названии ломает поиск TMDB.**
«Простите за беспокойство **2018**», «Одиссея **2026**» — TMDB не находит.
**Фикс:** в `clean_title()` срез `\s+(?:19|20)\d{2}\)?$`, и в `tmdb_search()`
год переносится в `year`-хинт.
2. **Ключ кэша должен включать год** (`title|year`), иначе «Дракула 1992» и
«Дракула 2025» схлопнутся в одну запись.
3. **Распознавание media_type:** искать и `movie`, и `tv` (аниме/сериалы в списке есть).
### Ключевой факт про `href` в каталоге (из `ListController.cs:314–322`)
```csharp
else if (href.StartsWith("http"))
href = Regex.Replace(href, "https?://[^/]+/", ""); // вырезает домен
```
Поэтому `https://www.themoviedb.org/movie/687163` → `id = "movie/687163"` →
`CardController` получает полноценный TMDB-URI → **карточка с постером, жанрами,
описанием подтягивается штатным механизмом Lampa.** Работает для `movie/N` и `tv/N`.
### Проверка результата (то, что должен увидеть Alex)
```bash
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films&page=1" \
| jq '{total: (.results|length),
posters: ([.results[]|select(.img)]|length),
tmdb: ([.results[]|select(.id|test("^(movie|tv)/"))]|length)}'
```
## 🟢 РЕШЕНИЕ: первая категория игнорирует `format` (2026-09-22, проверено)
**Это была настоящая причина «здесь пусто» в UI.** Читать первым, если каталог отдаёт
названия, но без постеров и с `id` = ссылкой на поиск Кинопоиска.
**Симптом (замер):**
```
cat=films → 360 элементов, постеров 0, id = "index.php?kp_query=…"
cat=films&sort=rating → 1 элемент (правильный файл!)
cat=new → 33 ✅ cat=cartoons → 3 ✅ cat=anime → 7 ✅
```
Диагноз: `cat=films` читала **`watchlist.json`** (общий файл, 360 = `channels.films.items`),
а не `watchlist-films.json`. То есть **блок `format` для неё не применился**.
**Причина — по коду `ListController.cs`:**
```csharp
// строка 50: базовый URL
string url = $"{init.host}/{(page == 1 && init.list?.firstpage != null
? init.list?.firstpage : init.list?.uri)}";
// строки 59-85: ветка при непустом cat
var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat));
string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-");
if (!string.IsNullOrEmpty(eval))
url = CSharpEval.BaseExecute<string>(eval, new CatalogGlobalsMenuRoute(...));
```
Категория, **стоящая ПЕРВОЙ** в списке `categories`, до применения `format` уже получает
базовый URL из `list.uri`. Пока `Фильмы` была первой, она читала `list.uri`
(= `watchlist.json` при том значении, что стояло) вместо своего `format`-URL.
**Фикс (одна перестановка):**
```yaml
menu:
- categories:
Новинки: new # ← первой ставим НОВИНКИ
Фильмы: films # (она читает list.uri = watchlist-new.json)
Мультфильмы: cartoons
Аниме: anime
format:
"-": |
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
```
⚠️ **`list.uri` и `list.firstpage` обязаны совпадать с файлом первой категории**
(здесь — `watchlist-new.json`), иначе первая категория отдаст чужой файл.
В генераторе это зафиксировано комментарием-питфоллом.
**Проверка после правки (все 4 должны дать свои числа, а не общий список):**
```bash
for c in new films cartoons anime; do
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=$c&page=1" \
| jq -r "\"$c: \(.results|length) элементов, постеров \([.results[]|select(.img!=null)]|length)\""
done
```
## 🔴 ГЛАВНЫЙ ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН, он генерируется сам (2026-09-22)
**Симптом:** добавил блок `main:` в `sites/watchlist.yaml` → «Мой список» **полностью
исчез** из `/catalog` (остались только Eneyida, FilmixApp, KinoUkr, Kinobase, Rezka, VoKino).
Запрос `catalog/list?plugin=watchlist` отдавал **пустоту**. В логах Lampac — **тишина**,
ошибка не пишется, сайт просто отбрасывается.
**Причина:** `CatalogSettings` **не имеет поля `main`** для входного YAML. YamlDotNet
давится неизвестным полем → `ModInit.goInit(site)` возвращает **null** →
`ApiController.cs:70` отбрасывает сайт:
```csharp
if (init == null || !init.enable || init.menu == null || init.hide)
continue;
```
⚠️ Проверено обе формы — **обе ломают**:
```yaml
main: # список — НЕ работает
- categories:
Фильмы: films
```
```yaml
main: # словарь — НЕ работает
Фильмы: http://...
```
**Как на самом деле:** Catalog **сам генерирует `main`** в JSON-ответе `/catalog`,
выводя его из блока `menu`. Проверено — в ответе есть **и `menu`, и `main`** с одинаковым
содержимым:
```json
"menu": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... },
"main": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... }
```
**Писать `main` в YAML не нужно и нельзя.** Достаточно `menu`.
### Правильная форма YAML (проверенная, рабочая)
```yaml
enable: true
jsonPath: true
displayindex: 1
displayname: Мой список
host: http://127.0.0.1:9118
menu: # ← ЕДИНСТВЕННЫЙ обязательный блок категорий
- categories:
Фильмы: films
Новинки: new
Мультфильмы: cartoons
Аниме: anime
format:
"-": |
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
search:
uri: watchlist.json
list:
uri: watchlist-films.json
firstpage: watchlist-films.json
total_pages: 1
content:
nodes: "$.items[*]"
name: { attribute: name }
original_name: { attribute: original }
year: { attribute: year }
href: { attribute: href }
image: { attribute: image }
args:
- { name_arg: rating, attribute: rating }
```
### Сопутствующие питфоллы (все проверены на живом сервере)
1. 🔴 **`menu` ОБЯЗАТЕЛЕН.** Не «подменяет разделы», а **регистрирует канал**.
`ApiController.cs:70`: без `menu` сайт отбрасывается. Убирать нельзя.
2. 🔴 **`host` + `uri` СКЛЕИВАЮТСЯ.** `ListController.cs:53`:
```csharp
string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
? init.list?.firstpage : init.list?.uri)}";
```
Значит `uri`/`firstpage` должны быть **относительными** (`watchlist-films.json`).
Полный URL даёт двойной префикс → пусто:
`http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json`
3. 🔴 **Явно указанный `format` в `menu` вызывает перезагрузку страницы при каждом
выборе категории.** Если не нужен — убрать `format` из `menu`, оставить только
`categories`. (Наблюдение Alex: «перезагружает прогрузку страницы при каждом выборе
раздела».)
### Кто на самом деле подменял родные разделы Lampa
**НЕ YAML.** Это `plugin.js` (он же `/catalog.js`):
```js
Lampa.Listener.follow('menu', function(e){
if (e.type=='action' && (e.action=='tv'||'cartoon'||'anime'||'movie'||'relise')) {
var catalog = Api.getCatalog();
if (!catalog) return;
...
Lampa.Router.call('category', {...});
e.abort(); // ← перехват клика по родному разделу
}
});
```
**Перехват срабатывает ТОЛЬКО когда выбран источник «Мой список».**
Родной CUB/tmdb не трогается, пока источник не переключён на наш.
### Как реально добиться «CUB + мой список сверху»
| Путь | Реализуемость |
|---|---|
| Блок `main` в YAML | ❌ **невозможно** — ломает парсинг |
| Оставить `menu` | ⚠️ источник работает, но разделы подменяются, пока он выбран |
| Патч `plugin.js` | ⚠️ возможно: убрать `Listener.follow('menu')` — перехвата не будет, но и секции сверху не появятся |
**Открытый вопрос для Alex:** родной CUB и «Мой список» — это **два источника Lampa**,
переключаются в «остальное → основной источник». Одновременно показывать нельзя без
патча `plugin.js`. Решение за Alex.
## ✅ РЕШЕНИЕ НАЙДЕНО 2026-09-22 — полная архитектура (проверено)
### Что уже есть в системе (нашёл, проверено)
**Модуль `watchlist_sync/resolver.py`** (`/srv/.../watchlist-sync/`) — **уже решает задачу
постеров и ID.** Умеет:
- поиск через **TMDB** (`api.themoviedb.org/3`) в `ru-RU` и `en-US`
- поиск через **Кинопоиск** (`api.poiskkino.dev/v1.4`)
- возвращает `Candidate`: `tmdb_id`, `kp_id`, `title`, `year`, `score`, `rating`, **`genres`**
- `is_horror()` — определяет ужасы по TMDB `genre_id=27` (для сортировки 🧟‍♀️)
- кэш в `.resolve_cache.json`
- различает `ok` / `ambiguous` (score < 0.75) / `not_found`
**Модуль `watchlist_sync/arr.py`** — клиент Radarr/Sonarr. 🔴 **ВОТ ИСТОЧНИК СКАЧКИ:**
```python
"monitored": True,
"addOptions": {"searchForMovie": True}, # ← качает сразу после добавления
```
Для сериалов: `"addOptions": {"searchForMissingEpisodes": True}` — то же самое.
### Ключевая связка ID
| База | Где есть | Формат |
|---|---|---|
| **Кинопоиск (kp)** | в `.md`: 68 строк со ссылками `kinopoisk.ru/film/<id>/` | `11825624` |
| **TMDB** | в Radarr (`tmdbId`), в resolver (`Candidate.tmdb_id`) | `1292695` |
| **IMDb** | в Radarr (`imdbId`), в `.md` только рейтинг | `tt31728330` |
⚠️ **Прямой связи kp↔tmdb в файле нет.** Связка делается через **`resolver.py`**: он по
названию находит и `kp_id`, и `tmdb_id` одновременно. Это его основная работа.
### Замер Radarr (после шагов 1–3, 2026-09-22)
| Метрика | Значение |
|---|---|
| Фильмов | **50** |
| `monitored` | **0** ← автоскачка выключена |
| `rssSyncInterval` | **0** ← в RSS не ходит |
| Очередь | **0** |
| С `tmdbId` | 50 (все) |
| С постером | **46** |
| С рейтингом TMDB | 50 (все) |
| С файлом на диске | 24 (не трогали) |
Sonarr: 13 сериалов, `monitored` 0, RSS 0, очередь 0.
### Поля Radarr для каталога (проверено, `GET /api/v3/movie`)
```json
{
"tmdbId": 1292695,
"imdbId": "tt31728330",
"title": "They Will Kill You",
"originalTitle": "They Will Kill You",
"year": 2026,
"titleSlug": "1292695",
"hasFile": true,
"genres": ["Horror"],
"ratings": {"tmdb": {"value": 6.945}},
"images": [{"coverType": "poster",
"remoteUrl": "https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28Ruu6.jpg"}]
}
```
**Названия в Radarr — латиницей** (`They Will Kill You`), в `.md` — по-русски. Перевод
делает **TMDB Proxy** (`proxy_tmdb = true` в `lampainit.js`) — уже включён.
### КАК ИГРАЕТ LAMPA (проверено — на диск НЕ пишется)
| Компонент | Значение | Смысл |
|---|---|---|
| TorrServer | `/ts/echo` → `MatriX.135` | жив, встроен в Lampac |
| **`UseDisk`** | **`false`** | 🔴 **на диск НЕ пишет** |
| `CacheSize` | 100663296 (96 МБ) | кэш в RAM |
| `TorrentsSavePath` | `""` (пусто) | пути сохранения нет |
| `TorrentDisconnectTimeout` | 180 | раздача живёт 3 мин после стопа |
| Занятые торренты | `[]` (пусто) | ничего не висит |
**Поиск торрентов:** Lampa → `jackett_url = 192.168.1.14:9118` → **Lampac сам отдаёт
Jackett-совместимый API**:
```bash
curl "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix"
# → {"Results":[{"Tracker":"rutor","Title":"...","MagnetUri":"magnet:?xt=...","Seeders":0}]}
```
✅ Проверено: вернул реальные результаты. Отдельный Jackett **не нужен** (его нет в системе).
🔴 **`parser_torrent_type = 'jackett'`** в `lampainit.js` — это НЕ отдельный Jackett,
а эндпоинт внутри Lampac. Так и задумано производителем.
### Итоговая архитектура (то, что нужно поставить)
```
family/documents/movies-watchlist.md ← источник (360 фильмов)
│
├─► resolver.py ──► TMDB + Кинопоиск
│ │ tmdb_id + постер + рейтинг + жанры
│ ▼
│ wwwroot/watchlist-<cat>.json ← каталог ДЛЯ LAMPA (с постерами!)
│ │
│ ▼
│ sites/watchlist.yaml (main: только секции СВЕРХУ, БЕЗ menu)
│ │
│ ▼
│ Lampa: плагин catalog.js → секции с постерами СВЕРХУ родного CUB
│
└─► arr.py (searchForMovie: False) ──► Radarr/Sonarr
└─► метаданные в базу БЕЗ скачки
Просмотр: Lampa → TorrServer (RAM, UseDisk:false) → на HDD ничего
```
**Разделение ролей:**
- **resolver.py** — постеры/ID для **каталога** (360 фильмов, включая те, что не в Radarr)
- **Radarr** — справочник того, что уже добавлено (50), источник `hasFile`
- **TorrServer** — воспроизведение, на диск не пишет
### Три правки (на апруве)
| # | Файл | Правка | Зачем |
|---|---|---|---|
| 1 | `watchlist_sync/arr.py` | `searchForMovie: True` → **`False`** (+ у дочки то же для сериалов) | добавление без скачки |
| 2 | ~~`catalog/sites/watchlist.yaml` — убрать блок `menu`, оставить `main`~~ ❌ **НЕВОЗМОЖНО** (опровергнуто тестом). Вместо этого — выбрать путь из трёх: оставить как есть / патч `plugin.js` / свой плагин. Решение Alex | ❌ отменено |
| 3 | генератор каталога | брать постер/ID через `resolver` | реальные элементы вместо заглушек |
⚠️ Правка №1 **может уже не понадобиться**: шаги 1–3 (unmonitor + RSS 0) закрыли скачку
на уровне состояния. Но `searchForMovie: True` в коде **включит её снова** при следующем
добавлении из списка. Поэтому фикс нужен — иначе вернётся.
## 🎯 ПЕРЕДЕЛКА 2 — Radarr как источник метаданных без автоскачки (план Alex, 2026-09-22)
### Логика Alex (дословно)
> «Мы блядь убираем автоскачку на диск так?! При этом ты оставляешь radarr/sonarr
> источником, так?? И в него из списка добавляется скриптом, но не грузит автоматом так?
> Сейчас ты отменяешь текущие загрузки, так??»
**Расшифровка:**
| # | Что | Как |
|---|---|---|
| 1 | **Автоскачка на HDD — выключить** | Radarr/Sonarr ничего не качают сами |
| 2 | **Radarr/Sonarr остаются источником** | TMDB ID + постеры для каталога Lampa |
| 3 | **Скрипт добавляет в них из списка** | `.md` → Radarr API, фильм получает метаданные |
| 4 | **Но не грузит автоматом** | Добавление ≠ загрузка |
| 5 | **Текущие загрузки — отменить** | 6 записей в очереди |
### Как это в Radarr технически (проверено 2026-09-22)
⚠️ **Важно для понимания:** в Radarr «добавить, но не качать» = **`monitored: false`**.
Отдельного состояния «источник, но не грузить» нет. При добавлении через API Radarr
**сам ходит в TMDB** и тянет метаданные (постер, год, описание) — даже при `monitored: false`.
Это и есть решение задачи №2+№3+№4.
### Доступ к Radarr API (проверено 2026-09-22, воспроизводимо)
```bash
# API-ключ лежит в config.xml (не в БД)
ssh kraken 'grep -oE "<ApiKey>[^<]+</ApiKey>" \
/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker/radarr/config/config.xml' \
| sed 's/<[^>]*>//g' # → cbcb8ec320104359920c0f611d503c7b
```
🔴 **ПИТФОЛЛ: `-L` обязателен.** Без него Radarr отдаёт **307 с пустым телом** и `jq length` падает:
```bash
curl -s "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # → пустой вывод, 0 байт
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # → 200, 50
```
**Конфиг Radarr (замер):**
```bash
curl -sL ".../api/v3/config/indexer?apikey=$KEY" | jq .rssSyncInterval # → 30 (мин)
```
`rssSyncInterval: 30` = Radarr **каждые 30 минут** сам ходит в RSS-фиды и качает новое.
Это вторая причина автоскачки помимо `monitored: true`. **Выключать надо оба.**
### Окружение Radarr на Кра́кене (для будущих сессий)
| Параметр | Значение |
|---|---|
| Порт | `7878` (`0.0.0.0`) |
| Контейнер | `radarr`, образ `lscr.io/linuxserver/radarr:latest` |
| Конфиг на HDD | `/srv/...6194539b.../docker/radarr/config/` |
| БД | `radarr.db` (+ `-shm`, `-wal`) |
| Постеры | `/config/MediaCover/` — **46 каталогов** |
| `sqlite3` в контейнере | ❌ **нет** (на хосте Кра́кена — **есть**, `/usr/bin/sqlite3`) |
| Соседи | `sonarr:8989`, `prowlarr:9696`, `transmission` |
⚠️ В контейнерах `sqlite3` отсутствует (см. профиль Alex) — БД читать через `strings`
или вытаскивать файл наружу и смотреть хостовым `sqlite3`.
### Текущее состояние Radarr (замер 2026-09-22)
| Метрика | Значение |
|---|---|
| Фильмов всего | 50 |
| `monitored: true` | **45** ← активны, ищут и качают |
| `hasFile: true` | 24 (уже на диске) |
| `rssSyncInterval` | **30** мин ← Radarr ходит в RSS каждые полчаса |
| Очередь | **6 записей** (все `warning`, ничего не качается активно) |
| Transmission | простаивает (1% CPU) |
Очередь (id для отмены):
```
598407147 Brokeback Mountain.mkv
1092005446 Гладиатор (2000) (DTS X) DV.mkv
914899324 Sorry.to.Bother.You.BDRemux.1080p.NNMClub.pk.mkv
1671545331 Мортал Комбат 2 (DTS-HD MA).mkv
1486734395 Gorod.boga.2002.BDRemux.1080p.mkv
139445264 День хомячка (2003)
```
## ✅ ПРОВЕРКА «как это реализуемо» — полный аудит 2026-09-22 (вечер)
Alex: «Мне надо чтоб lampa источники видела и играла фильмы из списка и cub.
При этом мы перестаем качать торренты на диск! Как это реализуемо??
Проверь блядь все чтобы ты опять не обосрался».
Проверено всё, что может качать или мешать. **Ничего не менялось — только чтение.**
### 🎯 Главная находка: TorrServer на диск НЕ пишет
Ключевой факт, снимающий главный вопрос Alex («при этом фильмы не будут
скачиваться на hdd я правильно понимаю?!»). Конфиг TorrServer внутри Lampac:
```bash
ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq .
```
```jsonc
{
"BitTorr": {
"CacheSize": 100663296, // 96 МБ — RAM-кэш
"UseDisk": false, // 🔴 НА ДИСК НЕ ПИШЕТ
"TorrentsSavePath": "", // путь сохранения пуст
"TorrentDisconnectTimeout": 180, // раздача живёт 3 мин после остановки
"PreloadCache": 0,
"ReaderReadAHead": 83
}
}
```
**`UseDisk: false`** — TorrServer держит всё в RAM (96 МБ) и **на диск не пишет ничего**.
Это ровно то, что нужно Alex. Настраивать не надо, уже так.
### TorrServer жив и работает (проверено)
```bash
curl -s "http://192.168.1.14:9118/ts/echo" # → MatriX.135 (версия, живой)
curl -s "http://192.168.1.14:9118/ts/torrents" # → 404 (это не тот эндпоинт)
curl -s -X POST "http://192.168.1.14:9118/ts/torrents" \
-H 'Content-Type: application/json' -d '{"action":"list"}' # → [] пусто, ничего не висит
```
Кэш Lampac: `/lampac/cache/` = 31 МБ (SQLite, Playwright, gstreamer-registry) —
**не фильмы**. `/lampac/data/` = 44 МБ (ts/settings.json, dlna/thumbs). Диск не забит.
### Поиск торрентов — Jackett внутри Lampac, отдельный НЕ нужен
🔴 **ПИТФОЛЛ-ЛОВУШКА: в `lampainit.js` стоит `parser_torrent_type = 'jackett'`,
но отдельного контейнера Jackett НЕТ.** Это не ошибка — Lampac **сам отдаёт
Jackett-совместимый API**.
```bash
# что прописывает Lampac клиенту:
curl -s "http://192.168.1.14:9118/lampainit.js" | grep -oE "Lampa.Storage.set\('[^']+', ?'[^']*'\)"
# Lampa.Storage.set('jackett_url', '192.168.1.14:9118')
# Lampa.Storage.set('jackett_key', '1')
# Lampa.Storage.set('parser_torrent_type', 'jackett')
# Lampa.Storage.set('parser_use', 'true')
```
`jackett_url` указывает **на сам Lampac**, а не на сторонний сервис. Проверка API:
```bash
# ⚠️ работает БЕЗ префикса /jackett — на корне
curl -s "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix"
# → 200, реальные результаты: rutor, магнит-ссылки, Size, Seeders, ffprobe
curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.14:9118/jackett/api/v2.0/..." # → 404
curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.14:9118/api/v2.0/..." # → 200 ✅
```
**Вывод:** поиск торрентов работает штатно, ничего поднимать не надо.
### Кто качает на диск — полный реестр (единственные виновники)
```bash
ssh kraken 'docker ps --format "{{.Names}}\t{{.Image}}"'
```
| Сервис | Качает? | Замер | Нужен для задачи? |
|---|---|---|---|
| **Radarr** | ✅ да | 50 фильмов, **45 monitored**, очередь **6**, RSS 30 мин | ❌ мешает |
| **Sonarr** | ✅ да | 13 сериалов, **13 monitored**, очередь **19** | ❌ мешает |
| **Transmission** | ⚠️ исполнитель | **257** `.torrent`, CPU 0.76% (простаивает), NetIO 290G/26.7G | ⚠️ движок Radarr/Sonarr |
| **TorrServer** | ❌ НЕТ | `UseDisk: false`, список торрентов `[]` | ✅ **вот чем играет Lampa** |
Полный список контейнеров Кра́кена:
`lampac · radarr · jellyfin · transmission · portainer · flaresolverr ·
prowlarr · sonarr · watchtower · rclone · xray-reverse-bridge · hermes-kraken · cloudflared`
**Sonarr API** (для будущей работы, по аналогии с Radarr):
```bash
# ключ: /srv/.../docker/sonarr/config/config.xml → 15fbec32f1cb4b05b1c2b229ce27359b
curl -sL "http://localhost:8989/api/v3/series?apikey=$SONARR_KEY" | jq 'length' # → 13
curl -sL "http://localhost:8989/api/v3/queue?apikey=$SONARR_KEY" | jq '.totalRecords' # → 19
```
⚠️ Sonarr — **тоже источник автоскачки**, не только Radarr. Alex упомянул его не зря.
Отключать надо оба, иначе Sonarr продолжит качать сериалы.
### Transmission — настройки (замер)
```bash
ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.json
```
```json
{"download-dir": "/downloads", "rpc-port": 9091,
"speed-limit-down-enabled": false, "peer-port": 51413,
"alt-speed-enabled": false}
```
`/config/torrents/*.torrent` = **257 файлов** — это накопленный список раздач,
**не признак активной качки** (CPU 0.76%). Файлы на диске не трогаем.
### Кто чем играет в Lampa (подтверждено)
```bash
curl -s "http://192.168.1.14:9118/lampainit.js" | grep -iE "torrents_use|source"
# lampa_settings.torrents_use = true ← кнопка торрентов включена
# Lampa.Storage.set('source', 'cub') ← CUB — основной источник ✅
# Lampa.Storage.set('proxy_tmdb', 'true')
```
В `plugins_add` (серверные плагины Lampac, не список Lampa):
`tmdbproxy.js · cubproxy.js · online.js · catalog.js · sisi.js · startpage.js ·
timecode.js · ts.js` — все `status: 1`.
### Итоговая схема (что получится после переделки)
```
ТВ: Lampa (com.lampac.tv)
├── источник CUB ── онлайн, работает как раньше ✅
└── источник «Мой список» ── секции СВЕРХУ, постеры из Radarr
│
▼ клик по фильму
поиск торрента через Lampac (Jackett-API на :9118)
│
▼
TorrServer (UseDisk: false) → RAM-кэш 96 МБ → на HDD НЕ ПИШЕТ ✅
Radarr/Sonarr после переделки = ЧИСТЫЙ СПРАВОЧНИК МЕТАДАННЫХ
(monitored: false + rssSyncInterval: 0) — не ищут, не качают
```
### ✅ Порядок работ — ШАГИ 0–3 ВЫПОЛНЕНЫ 2026-09-22
| # | Действие | Сервис | Статус |
|---|---|---|---|
| 0 | Бэкап `radarr.db` + `config.xml` + `sonarr.db` + `config.xml` | оба | ✅ **сделано** |
| 1 | Отменить очередь: 6 Radarr + 19 Sonarr | оба | ✅ **сделано** (оба 0) |
| 2 | `monitored: false` — 45 Radarr + 13 Sonarr | оба | ✅ **сделано** (оба 0) |
| 3 | `rssSyncInterval: 0` | оба | ✅ **сделано** (оба 0) |
| 4 | Проверка: monitored 0, очередь 0, RSS 0, Transmission тих | оба | ✅ **сделано** |
| 5 | Генератор каталога → постеры/ID через **TMDB + Radarr** (`gen_watchlist_v2.py`) | — | ✅ **написан, прогнан** (330/403 постеров) |
| 6 | ~~В `sites/watchlist.yaml` убрать `menu`, оставить `main`~~ ❌ **ОТМЕНЕНО** — невозможно, см. «🔴 ПИТФОЛЛ YAML» | — | ❌ опровергнуто |
| 6b | **Порядок категорий в `watchlist.yaml`:** «Новинки» первой, `list.uri` = `watchlist-new.json` | — | ✅ **исправлено** |
| 7 | Фикс `plugin.js` (`enumerable:true`) | — | ✅ **файл пропатчен**, заливка ждёт |
| 8 | Фикс `arr.py`: `searchForMovie` → `False` | — | ✅ **сделано, коммит `9872016`** |
| 9 | Заливка на Кра́кен + пересоздание контейнера + приёмка в UI | — | ⏳ `deploy_lampac.py` готов, не запущен |
**Не трогаем:** Prowlarr, Transmission, 24 файла на диске, Jellyfin, остальные контейнеры.
### 📋 Как выполнены шаги 1–3 (скрипты и команды — воспроизводимо)
**Шаг 0 — бэкапы (метка `20260922-123404`)**
```bash
D=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker
TS=$(date +%Y%m%d-%H%M%S)
sudo cp $D/radarr/config/radarr.db $D/radarr/config/radarr.db.bak-$TS
sudo cp $D/radarr/config/config.xml $D/radarr/config/config.xml.bak-$TS
sudo cp $D/sonarr/config/sonarr.db $D/sonarr/config/sonarr.db.bak-$TS
sudo cp $D/sonarr/config/config.xml $D/sonarr/config/config.xml.bak-$TS
```
Результат: `radarr.db` 4 374 528 Б, `sonarr.db` 4 870 144 Б + оба `config.xml` 650/648 Б.
**Шаг 1 — очистка очереди.** Скрипт `/tmp/arr_clear_queue.py` (на Mac → `scp` → запуск на Кра́кене).
`DELETE /api/v3/queue/<id>?removeFromClient=true&blocklist=false`.
Результат: Radarr 6 → **0**, Sonarr 19 → **0**.
**Шаг 2 — снятие monitored.**
- Radarr: `PUT /api/v3/movie/editor` с ключом **`movieIds`** → 45 → **0**
- Sonarr: `PUT /api/v3/series/editor` с ключом **`seriesIds`** → 13 → **0**
Скрипты: `/tmp/arr_disable_autograb.py` (Radarr, упал на Sonarr с HTTP 500),
`/tmp/sonarr_unmonitor.py` (Sonarr, исправленный ключ).
**Шаг 3 — `rssSyncInterval: 0`.** Скрипт `/tmp/radarr_rss_off.py`.
Sonarr: 15 → 0 с первого раза. Radarr: **со второго** — первый заход вернул 30
(не отправлял `id`), фикс — отправлять **весь объект конфига с `id: 1`**.
### 📊 Состояние ПОСЛЕ выполнения (замер 2026-09-22)
| Метрика | Radarr | Sonarr |
|---|---|---|
| Всего записей | 50 фильмов | 13 сериалов |
| `monitored` | **0** (было 45) | **0** (было 13) |
| `rssSyncInterval` | **0** (было 30) | **0** (было 15) |
| Очередь | **0** (было 6) | **0** (было 19) |
| С `tmdbId` | 50 (все) | — |
| С постером | **46** | — |
| С рейтингом TMDB | 50 (все) | — |
| С файлом на диске | 24 (не трогали) | — |
**Проверка пассивности:** Transmission за 45 секунд — NET `290GB/26.7GB` и
BLOCK `214GB/393kB` **не сдвинулись ни на байт**. Логи Radarr (`grep search|download|grab`)
за 2 минуты — **пусто**. TorrServer: список торрентов `[]`, `echo` → `MatriX.135`.
### 🔴 Питфоллы выполнения (найденные в этой сессии)
1. **`urllib` НЕ идёт по 307 автоматически, а `curl -sL` — идёт.**
Симптом: все 25 DELETE в первом прогоне вернули `HTTP Error 307: Temporary Redirect`,
**ничего не удалилось**, состояние не изменилось.
**Фикс:** свой `HTTPRedirectHandler` подкласс, который для 307/308 сохраняет
**метод и тело** (штатный `urllib` превращает DELETE в GET):
```python
class RedirectKeepMethod(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
if code in (307, 308):
new = urllib.request.Request(newurl, data=req.data,
method=req.get_method())
for k, v in req.headers.items():
new.add_header(k, v)
return new
return super().redirect_request(req, fp, code, msg, headers, newurl)
```
Тот же корень, что у `curl` без `-L` (питфолл в памяти) — только на стороне Python.
2. **Sonarr группирует эпизоды сезона под одним `id`.**
Симптом: после удаления первой записи остальные возвращают `HTTP 404: Not Found`.
Это **не сбой** — они исчезли вместе с родительской. Итог всё равно 0.
3. **Ключ поля в `*/editor` различается между Radarr и Sonarr.**
Radarr `/movie/editor` → **`movieIds`**; Sonarr `/series/editor` → **`seriesIds`**.
`ids` → HTTP 500 Internal Server Error (Sonarr).
4. **`PUT /api/v3/config/indexer` требует весь объект с `id`.**
Частичный `{"rssSyncInterval": 0}` молча возвращает старое значение.
Отправлять весь объект, полученный `GET`, с изменённым полем.
5. **Кириллица в inline-`jq` внутри `ssh '...'`** снова сломала команду
(`unexpected INVALID_CHARACTER`) — 6 ошибок в одном вызове. Ключи объекта
писать латиницей (`total`, `monitored`, `queue`), не `всего`/`очередь`.
### ❓ Открытый вопрос к Alex — ЗАКРЫТ
Был вопрос «Radarr/Sonarr нужны как справочник или выключаются совсем?».
**Alex ответил резко:** «Как ты блядь думаешь мне нужны блядь постеры или нет блядь?!
Меня не ебет откуда оно их берет сделай чтобы работало как поставлено в задаче».
→ **Остаются как справочник метаданных.** Вопрос постеров больше не задавать.
### План работ (на апруве Alex) — ШАГИ 0–4 ВЫПОЛНЕНЫ
**Шаг 0 — бэкап (обязательно, до всего)**
```bash
ssh kraken 'cp /srv/.../docker/radarr/config/radarr.db /srv/.../radarr.db.bak-$(date +%Y%m%d-%H%M%S)'
ssh kraken 'cp /srv/.../docker/radarr/config/config.xml /srv/.../config.xml.bak-$(date +%Y%m%d-%H%M%S)'
```
Причина: `radarr.db` — боевая база, содержит всю библиотеку.
**Шаг 1 — отменить текущие загрузки (6 шт)**
```bash
curl -sL -X DELETE "http://localhost:7878/api/v3/queue/<id>?removeFromClient=true&blocklist=false&apikey=$KEY"
```
⚠️ **Не** использовать `blocklist=true` — иначе релизы попадут в чёрный список и
не найдутся при ручной загрузке. Файлы на диске **не трогаются**.
**Шаг 2 — снять `monitored` со всех фильмов**
```bash
curl -sL -X PUT "http://localhost:7878/api/v3/movie/editor?apikey=$KEY" \
-H "Content-Type: application/json" \
-d '{"movieIds":[<все 50>],"monitored":false}'
```
Результат: Radarr не ищет и не качает. Фильмы в базе с метаданными остаются.
**Шаг 3 — выключить авто-поиск по RSS**
```bash
curl -sL -X PUT "http://localhost:7878/api/v3/config/indexer?apikey=$KEY" \
-H "Content-Type: application/json" \
-d '{"rssSyncInterval":0,"id":1, ...остальные поля как есть}'
```
0 = Radarr не ходит в RSS. Ничего не находит сам.
**Шаг 4 — проверить**
```bash
jq '[.[] | select(.monitored==true)] | length' # → 0
jq '.rssSyncInterval' # → 0
jq '.totalRecords' # очередь → 0
```
**Что НЕ трогаем:** Prowlarr, Transmission, библиотеку на диске (24 файла), Sonarr
(отдельная проверка ниже).
### ⚠️ Незакрытые вопросы к плану
1. **Sonarr** — Alex упомянул его вместе с Radarr. Проверить его настройки отдельно
(сериалы в списке есть?). Это отдельная задача. **Вопрос задан Alex, ответа нет.**
2. **Ручная загрузка после отключения** — если Alex захочет скачать фильм вручную,
нужно временно включить `monitored` или искать вручную через Interactive Search.
Работает, но не автоматически.
3. **310 фильмов, которых нет в Radarr** — скрипт будет добавлять их с `monitored: false`,
Radarr сам подтянет постеры из TMDB. Но часть названий найдётся криво («девять»).
### ✅ Подтверждено Alex 2026-09-22 (семантика задачи — не переспрашивать)
Alex сам сформулировал схему и подтвердил её понимание ассистентом:
> «При этом фильмы не будут скачиваться на hdd я правильно понимаю?! Играть будет
> через torrserver так?» → ответ: **да, правильно**.
**Разделение обязанностей (запомнить, чтобы не путать):**
| Слой | Роль | Куда пишет |
|---|---|---|
| **Radarr/Sonarr + Transmission** | библиотека, автоскачка релизов | на HDD (библиотека) |
| **Lampac TorrServer** | стриминг по требованию из UI Lampa | **ничего не сохраняет** — только временный кэш кусков |
| **«Мой список» (Catalog)** | только показ секций с постерами | ничего не качает |
То есть после выключения автоскачки в Radarr: **фильмы, которые Alex смотрит через Lampa,
на HDD не попадают вообще** — TorrServer отдаёт поток и выбрасывает буфер. Файлы на диске
остаются только у 24 уже скачанных (нужны Jellyfin).
⚠️ **`transmission` НЕ связан с Lampa.** Radarr качает через него релизы в свою библиотеку —
это отдельный контур. Вопрос «через что играет TorrServer» и «что качает Radarr» —
**разные вещи**, не смешивать в объяснениях.
### 🔴 Правка `plugin.js` (`enumerable:true`) — всё ещё НЕ применена
Независимая от ПЕРЕДЕЛКИ 2 правка. Файл лежит локально `/tmp/catalog-plugin.js`
(10738 байт, `node --check` чистый, 2 вхождения исправлены), бэкап
`/tmp/catalog-plugin.js.bak-20260922-121452`. На Кра́кене и в образе — **оригинал**.
Нужен volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` в compose.
Подробности — раздел «🔴 ГЛАВНЫЙ ПИТФОЛЛ» выше.
**Порядок:** правку `plugin.js` имеет смысл применять **вместе с ПЕРЕДЕЛКОЙ 2**,
чтобы не пересоздавать контейнер дважды.
### Откат
```bash
# вернуть monitored (одним запросом)
curl -sL -X PUT ".../api/v3/movie/editor" -d '{"movieIds":[...],"monitored":true}'
# вернуть RSS
curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1}'
# восстановить базу при необходимости
cp radarr.db.bak-<дата> radarr.db && docker restart radarr
```
**Статус: план на апруве. Ничего не выполнено.**
## Открытые вопросы — БЛОКИРУЮЩИЕ (2026-09-22, ждут решения Alex)
1. **Что делать с 310 фильмами, которых нет в Radarr?**
В `.md` — 360, в Radarr — 50. У 310 нет TMDB ID и постера.
Варианты: (а) показывать только 50 из Radarr; (б) добирать остальные через TMDB-search
(часть названий найдётся криво: «девять» → не то); (в) показывать заглушкой без постера.
**Без решения шаг 1 переделки не начать.**
2. **Нужны 4 секции или одна?** Секции «Фильмы / Новинки / Мультфильмы / Аниме»
или одна «Мой список» сверху. Вопрос задан, ответа нет.
## Открытые вопросы (не блокируют)
1. **Постеры** — тянуть через TMDB-кэш Lampac, через Radarr `MediaCover/`, или `remoteUrl` CDN?
2. **Карточка** — `card_parse` не настроен. С реальным TMDB ID карточка должна
подтянуться штатным механизмом Lampa — проверить после переделки.
3. **`🗓️` upcoming** — показывать или прятать? Сейчас показываем.
4. **`❓` ambiguous** — сейчас показываем с названием как есть.
## ⚠️ Отменённые/ошибочные указания (не повторять)
- ❌ **«Настройки → Источник»** — такого пункта в Lampa **не существует**. Выдумано,
Alex не нашёл. Источник переключается в разделе «остальное → основной источник»
(Alex нашёл сам 2026-09-22).
- ❌ **«влево → панель источников»** — тоже не подтверждено.
- ❌ **`plugins_add` в `lampainit.js`** — это серверные плагины Lampac
(`tmdbproxy.js`, `cubproxy.js`, `online.js`, `catalog.js`…), а **не** список плагинов Lampa.
`catalog.js` там есть при `"catalog": true` в init.conf.
## Связанные
- [[personal/plans/lampac-kraken]] — базовое развёртывание (шаги 1–6 сделаны)
- [[personal/tech/lampac-deployment]] — эксплуатация Lampac, модуль Catalog
- [[family/how-to/tv-lampac-plugin]] — подключение плагина на ТВ
- [[family/projects/watchlist-sync]] — парсер списка, CLI
- [[family/projects/watchlist-automation]] — форматы маркеров, cron
- [[family/documents/movies-watchlist]] — источник данных
## ✅ РАБОТАЕТ (2026-09-22, конец дня)
**Карточка фильма починена. Проверено: 339 из 339 карточек → HTTP 200.**
### Итог по каталогу
| Секция | Элементов | Постеров | Рабочих TMDB-ID |
|---|---|---|---|
| Новинки | 33 | 30 | 30 |
| Фильмы | 360 | 308 | 312 |
| Мультфильмы | 3 | 3 | 3 |
| Аниме | 7 | 7 | 7 |
| **Всего** | **403** | **348** | **352** |
Источники метаданных: Radarr 9, TMDB 343, Кинопоиск-фолбэк 51.
### Что сделано
1. Генератор `gen_watchlist_v2.py` — пишет **файл на каждый фильм**:
`wwwroot/cards/card-movie-NNN.json`, владелец `1000:1000`, режим 644.
2. `watchlist.yaml` → `card_parse.initUrl` собирает путь из `uri`:
`return $"{host}/cards/card-{uri.Replace("/", "-")}.json";`, `node: card`.
3. `deploy_lampac.py` — добавлен шаг `2b` (заливка `cards/` через tar), плюс
проверка карточки в шаге 7.
4. `arr.py` — `monitored: False`, `searchFor*: False` (коммит `9872016`).
5. `plugin.js` — патч `enumerable:true`, смонтирован volume, переживает обновление образа.
### 🔴 ПИТФОЛЛЫ, найденные по ходу (все стоили времени)
1. **Проверять карточку надо по `/catalog/card`, НЕ по `/card`.**
`/card` отдаёт 404 и выглядит как поломка каталога. Потерян час на ложном диагнозе.
2. **`parse.node` — СТАТИЧНЫЙ JSONPath.** `{uri}`/`{cat}` в него НЕ подставляются
(`json.SelectToken(parse.node)`). Слэш в ключе `movie/NNN` трактуется как вложенность
JSON → `SelectToken` возвращает null → `BadRequest("parse.node")`.
Именно поэтому выбран формат «файл на фильм» с фиксированным `$.card`.
3. **`card_parse.initUrl` ИСПОЛНЯЕТСЯ как C#** (`CSharpEval`, объект
`CatalogInitUrlCard` с полями `host/uri/type/args`). Только здесь можно подставить
`uri`. Кавычки внутри YAML-heredoc писать экранированно.
4. **`CardController.Index(plugin, uri, type)` — параметра `cat` НЕТ.**
`{cat}` в карточке всегда пустой.
5. **macOS `tar` кладёт в архив AppleDouble-мусор `._*`** — файлы попадают в
`wwwroot` и мешают. Использовать `tar --no-mac-metadata` + `rm -f ._*`.
6. **Права решают.** Если файлы отдать `root:root 600` — dotnet отвечает
`Permission denied`, статика молча отдаёт **404**. Нужно `1000:1000` + `755`/`644`.
7. **`find -exec` через `ssh -t` рвётся**: `missing argument to -exec`.
Использовать `chmod -R` отдельными вызовами.
8. **Счётчик TMDB-ID врал нулём**: `id` приходит как `movie/424781` (префикс),
а проверялось `"/movie/"` (вхождение со слэшем спереди). Проверять `startswith`.
9. **Первая категория в `menu.categories` игнорирует `format`** и берёт `list.uri`
напрямую. Поэтому «Новинки» стоят первыми, и их `list.uri = watchlist-new.json`.
10. **Ключи карточки.** `CardController` отдаёт `title`/`original_title`, а
`card_parse` ищет `name`/`original`. В JSON пишем **оба** набора ключей.
### Воспроизведение
```bash
cd /Users/admin/lampac-tmp
python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
python3 deploy_lampac.py /tmp/wl-out --skip-plugin # ~2 мин
```
Cron: генератор в **02:30**, после `watchlist-nightly` (01:00).
### Осталось на ТВ
Проверить глазами в Lampa на `192.168.1.70`: секции сверху с постерами,
клик → карточка с описанием → воспроизведение через TorrServer.
Приёмка = результат виден в UI.
### Не сделано (ждёт решения, без апрува не трогать)
310 фильмов из списка, которых нет в Radarr — пока показываются через фолбэк
Кинопоиска (51 без постера). Решить судьбу отдельно.
---
## 🔴 ФИНАЛЬНЫЙ ФИКС: карточка = «здесь пусто» без ЧИСЛОВОГО `tmdb_id`
**2026-09-22, третья сессия. Карточка заработала: 352/352 отдают `tmdb_id`.**
Alex после первого «готово»: «при открытии все так же "здесь пусто" и никакого инфо
или воспроизведения». Сервер отдавал `HTTP 200` с title/img/overview — значит
дело было **не в запросе, а в полноте ответа**.
### Причина — одна, но скрытая за двумя масками
`plugin.js` при клике (`full()`):
```js
if(json.tmdb_id){ status.need++;
Lampa.Api.sources.cub.reactionsGet(
{id: json.tmdb_id, method: json.original_name ? 'tv' : 'movie'}, ...); }
```
**Без `tmdb_id` Lampa считает карточку неполной → пустой экран.** Причём нужен
**числовой** `tmdb_id`, не строка.
Два бага в цепочке `card_args`:
1. **`ModInit.cs:285` — `nodeValue` возвращает ЛЮБОЕ `JValue` как строку.**
```csharp
string value = valueToken switch {
JValue jValue => jValue.Value?.ToString(), // ← число 424781 → "424781"
... };
```
Поэтому `tmdb_id` из JSON-файла приходил строкой, и `if(json.tmdb_id)` не
давала числа. **Никакой конфиг это не обходит — только `format`.**
2. **Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ процесса и переживает
`docker restart`.** После правки данных старые ответы продолжали отдаваться.
Именно поэтому `movie/424781` (**первый элемент «Фильмов»**, по нему Alex и
кликал) упорно оставался `null`, хотя исправленный файл уже лежал на диске.
**Проверять фикс только на НЕзакэшированном `uri`.**
### Фикс — `format` в `card_args` (исполняется как C#, возвращает настоящий тип)
```yaml
card_args:
- name_arg: tmdb_id
attribute: tmdb_id
format: "long.TryParse(value, out long v) ? v : 0"
- name_arg: source
attribute: media_type
- name_arg: overview
attribute: overview
- name_arg: genres
attribute: genres
- name_arg: runtime
attribute: runtime
format: "long.TryParse(value, out long v) ? v : 0"
- name_arg: vote_average
attribute: vote_average
format: "double.TryParse(value, out double v) ? v : 0"
- name_arg: rating
attribute: rating # остаётся строкой — Lampa показывает в бейдже
```
`format` вызывается как `CSharpEval.Execute<object>(nd.format,
new CatalogNodeValue(value, host), options)` (`ModInit.cs:255` и `:317`) —
`value` это уже строка, поэтому нужен `TryParse`.
### Эталонный рабочий ответ
```json
{ "id": "movie/424781", "title": "Простите за беспокойство",
"original_title": "Простите за беспокойство", "tmdb_id": 424781,
"source": "movie", "runtime": 111, "rating": "6.8", "vote_average": 68,
"genres": [{"name":"фэнтези"},{"name":"фантастика"},{"name":"комедия"}],
"overview": "...", "img": "http://192.168.1.14:9118/proxyimg:210:0/..." }
```
### Итог проверки
**352 из 352** карточек (все, у кого есть TMDB ID) отдают числовой `tmdb_id`,
`source`, `runtime`, рейтинг и жанры. Ошибок — 0.
### 🔴 CUB: «вижу только фильмы из списка»
CUB **не пропал** — это **два независимых источника Lampa**, одновременно
показать нельзя. Переключение: **Настройки → Остальное → Основной источник**.
Пункта «Источник» в настройках НЕ существует.
### Питфолл приёмки на ТВ
Lampa на ТВ **кэширует манифест и ответы**. После правок надо **полностью
выгрузить приложение** (не свернуть), иначе клиент держит старый кэш и
показывает дореформенный результат.
---
## 🔴 СЕССИЯ 4 (2026-09-22, поздняя ночь): приёмка НЕ пройдена, требование изменено
### Что сказал Alex
1. «у фильмов в "Мой список" появились норм названия, годы и у большинства —
постеры. **Но при открытии все так же "здесь пусто"** и никакого инфо или
воспроизведения!»
2. «че блядь значит надо Настройки → Остальное → Основной источник?! мы блядь
это уже обсуждали и **надо было добавить в CUB список новые секции** а не
ручное блядь переключение!»
### Статус по факту
| Пункт | Состояние |
|---|---|
| Серверная карточка | ✅ **исправна** — curl отдаёт `tmdb_id: 424781` числом, `source`, жанры, постер, описание |
| Приёмка на ТВ | ❌ **НЕ пройдена** — «здесь пусто» на экране |
| Соединения ТВ → Lampac | ❌ **ноль** (`sudo ss -tn \| grep 9118` пусто в момент проверки) |
| Секции внутри CUB | ❌ не реализовано — сейчас подмена через `e.abort()` |
### Диагностика, выполненная в этой сессии
Разобран `app.min.js` (2.1 МБ) — **эталон CUB-источника**:
- `CUB.full()` ставит `json.source = 'cub'` сам (строка ~34252).
- `Explorer` читает `this.object.movie || this.object.card`.
- `full` пушится с `source: element.data.source || 'tmdb'` (**строка 44291**) —
🔴 **фолбэк на tmdb, если у элемента нет поля `source`**. В ответе
`/catalog/list` поля `source` **нет** → проверить первым делом.
- `plugin.js:42-43` требует от карточки: `tmdb_id` (**число**), `original_name`
(или `original_title`) для определения tv/movie, `img`, `overview`.
Полная анатомия — [[personal/tech/lampac-deployment]] → «Анатомия CUB-источника».
### 🎯 ЗАДАЧА СЛЕДУЮЩЕЙ СЕССИИ (переформулирована Alex'ом)
**Не переключать источник, а дорисовать наши секции ВНУТРЬ CUB.**
1. **Проверить гипотезу `source`.** Элементы `/catalog/list` не содержат `source`.
Если `full` уходит в `tmdb` вместо `watchlist` — это и есть «здесь пусто».
Проверка: `curl "/catalog/list?plugin=watchlist&cat=films&page=1" | jq '.results[0].source'`.
2. **Отвязать плагин от перехвата `menu`.** Патчить рендер главной CUB
(`app.min.js` строки ~44260-44310: `feed`/`category_full`) либо добавить ряды
через `Lampa.Listener.follow('activity')`. `e.abort()` — НЕ использовать
(отменяет родной раздел, чего Alex не хочет).
3. **Приёмка = глазами на ТВ.** Открыть **обычный CUB** → сверху видны наши
секции с постерами → клик → карточка с описанием → кнопка воспроизведения
ведёт в TorrServer. Никакого ручного переключения источника.
### Питфоллы, найденные в этой сессии
- 🔴 **Проверять карточку по `/catalog/card`, НЕ по `/card`.** `/card` отдаёт 404
и выглядит как поломка каталога — потерян час на ложном диагнозе.
- 🔴 **Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ** и **переживает
`docker restart`**. После правки данных старые ответы продолжают отдаваться.
Проверять на **незэкешированном** `uri` (взять другой фильм).
- 🔴 **`nodeValue` (`ModInit.cs:285`) возвращает любое `JValue` СТРОКОЙ** →
в `card_args` нужен `format: "long.TryParse(value, out long v) ? v : 0"`.
- 🔴 **`tmdb_id` ДОЛЖЕН быть числом** — `plugin.js` делает `if(json.tmdb_id)`.
- macOS `tar` кладёт AppleDouble `._*` в `wwwroot` → `tar --no-mac-metadata`.
- Права `root:root 600` → `Permission denied` → статика молча отдаёт **404**.
Нужно `1000:1000` + `755`/`644`.
- `find -exec` через `ssh -t` рвётся (`missing argument to -exec`) → `chmod -R`.
- `&&`-цепочка рвёт tty-сессию ssh → каждая команда отдельным вызовом.
- `tcpdump` на Кра́кене **не был установлен** → `sudo apt-get install -y tcpdump`.
- `com.webos.service.devtools` **не существует** на webOS 5.4.2 Lite.
- `luna-send` пишет в tty → только через `script -q -c "..." /tmp/out.txt`.
### Артефакты
- Скрипты: `/Users/admin/lampac-tmp/{gen_watchlist_v2.py,deploy_lampac.py,watchlist.yaml}`.
- Копия UI Lampa для разбора: `/tmp/app-min.js` (2.1 МБ).
- Бэкап плана: `~/backup-lampac-plugin-plan-20260922-150459.md`.
- Доклады по карточке — [[personal/tech/lampac-deployment]] (конец файла).
## Связанные
## ✅ РАБОТАЕТ НА ТВ (2026-09-22, вечер) — подтверждено Alex
**«Фильмы из списка Watchlist теперь открываются и проигрываются.»**
### 🔴🔴 КОРНЕВАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО» — РУССКИЙ КЛЮЧ ПЛАГИНА
Поймано tcpdump'ом на Кра́кене, запрос с ТВ:
```
GET /catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=movie%2F1280015&type=movie
HTTP/1.1 400 Bad Request
```
`plugin=Мой список` — ключ каталога брался из `displayname`.
`ModInit.goInit` (строки 51-52):
```csharp
site = site.ToLowerAndTrim();
site = Regex.Replace(site, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase);
```
**Кириллица вырезается ЦЕЛИКОМ** → `site = ""` →
`File.Exists("/sites/.yaml")` = false →
`FindSiteByDisplayName("")` тоже мимо (пустой site не совпадёт с displayname) →
`return null` → **`init not found`** → HTTP 400 → в Lampa «ЗДЕСЬ ПУСТО».
🔴 **Fallback по `displayname` НЕ РАБОТАЕТ с кириллицей** — regex убивает имя
раньше, чем запускается поиск. Это баг/особенность lampac-nextgen.
**ФИКС:** задать `plugin: watchlist` в YAML. Строка `ApiController.cs:136`:
```csharp
string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site;
```
заданный `plugin` переопределяет ключ каталога → ТВ зовёт `plugin=watchlist` → файл находится.
**Приёмка:**
| Ключ | Ответ |
|---|---|
| `plugin=Мой список` | 400 `init not found` |
| `plugin=watchlist` | 200, `tmdb_id: 424781`, `source: "movie"` |
⚠️ **МЕТОДОЛОГИЧЕСКИЙ ВЫВОД (важно):** я проверял карточку своим `curl`'ом с
`plugin=watchlist` и был уверен, что всё работает. **Надо было ловить РЕАЛЬНЫЙ
запрос с устройства** (tcpdump на порту сервиса). Урок: при «сервер отвечает 200,
а в UI пусто» — первым делом снять трафик между устройством и сервером.
### Что сделано в этом заходе
1. `plugin: watchlist` в `sites/watchlist.yaml` — ключ каталога латиницей.
2. `card_args` с `format` для числовых полей:
`tmdb_id` → `long.TryParse`, `runtime` → `long.TryParse`,
`vote_average` → `double.TryParse`.
Причина: `ModInit.cs:285` `nodeValue` возвращает ЛЮБОЙ `JValue` как строку, а
`plugin.js` делает `if(json.tmdb_id)` — нужен настоящий `long`.
3. `deploy_lampac.py`: проверка ключа `watchlist` (не «Мой список»), проверка карточек
по `/catalog/card`, заливка `cards/` через tar.
4. `gen_watchlist_v2.py`: `tmdb_id` числом; дубли ключей
(`name`+`title`, `original`+`original_title`, `poster`+`img`).
## ⏳ ОСТАЛОСЬ ДОБИТЬ
1. **Несколько фильмов всё ещё «здесь ничего нет»** — «Рассекая волны», «Хамнет».
Похоже, для них нет `card-movie-NNN.json` (TMDB не нашёл / Кинопоиск-фолбэк
без tmdb id) → `initUrl` ведёт на несуществующий файл.
Лечить: фолбэк-карточка с тем, что есть (название+постер+поиск торрента),
либо дотягивать tmdb_id по точному названию+году.
2. **~51 фильм без постера** — TMDB не нашёл ни напрямую, ни через Кинопоиск.
Разобрать поштучно: возможно, помогают альтернативные названия (`original_title`).
3. **Секции CUB.** В «Мой список» только 3 секции (Фильмы/Мультфильмы/Аниме),
«Новинки» куда-то делись? И главное — **фильмов из CUB-фида в списке нет**.
Требование Alex: **секции должны быть ВНУТРИ CUB**, без ручного переключения
источника. Механизм: хук `Listener.follow('menu')` в `plugin.js` (строка 59)
перехватывает `movie`/`cartoon`/`anime`/`tv`/`relise` и делает `e.abort()`,
НО работает только когда активен наш источник (`Api.getCatalog()` читает
манифест текущего источника). Нужно доделать так, чтобы при источнике CUB
наши секции добавлялись к CUB-фиду.