---
title: Lampac — развёртывание и эксплуатация (Кра́кен)
created: '2026-09-22'
updated: '2026-09-22'
type: tech
namespace: personal
tags:
- lampac
- torrserver
- docker
- kraken
- streaming
- webos
related:
- '[[personal/plans/lampac-kraken]]'
- '[[personal/plans/lampac-watchlist-plugin]]'
- '[[personal/tech/multistream-single-flow-limit]]'
- '[[family/how-to/kraken-network]]'
- '[[family/how-to/tv-luna-send]]'
---
# Lampac — развёртывание и эксплуатация (Кра́кен)
> Технический справочник. Развёрнуто 2026-09-22. План и статус шагов — [[personal/plans/lampac-kraken]].
## Что это
`lampac-nextgen/lampac` (MIT, .NET 10) — серверный бэкенд для Lampa. Один контейнер, порт **9118**.
**Внутри (всё в одном образе):**
- **TorrServer** — subprocess, кэш раздач
- **JacRed** — Jackett-совместимый индексатор
- **LampaWeb** — веб-морда
- **TmdbProxy**, **DLNA**, **Sync**
🔴 **TorrServer встроенный. Отдельный контейнер не нужен.** Проверено 2026-09-22:
в логах первого старта видно `compilation TorrServer`. Внешний TS — лишняя сущность.
## Расположение на Кра́кене
```
/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/
├── docker-compose.yml (бэкапы .bak-*, .bak2-*)
├── config/ → /lampac/config
├── cache/ → /lampac/cache (кэш TorrServer — только сюда)
├── data/ → /lampac/data (БД JacRed)
├── config-module/init.conf → /lampac/init.conf:ro (наш конфиг)
├── catalog/sites/ → /lampac/module/Catalog/sites
├── catalog/override/ → /lampac/module/Catalog/override
├── catalog/plugin.js → /lampac/module/Catalog/plugin.js:ro (ПАТЧ, см. ниже)
├── wwwroot/ → /lampac/wwwroot (JSON-каталог watchlist + cards/)
└── scripts/gen_watchlist.py (генератор .md → JSON)
```
Локальные копии рабочих артефактов (на Mac, `/Users/admin/lampac-tmp/`):
`gen_watchlist_v2.py`, `patch_plugin.py`, `plugin-patched.js`, `watchlist.yaml`,
`docker-compose.yml`, `deploy_lampac.py`, **`build_plugin.py`** (сборщик патча секций),
**`plugin-cub.js`** (результат), `deploy_cub.py`, `tv_restart.sh`, `restart_lampa.sh`,
**`probe_*.py`** (CDP-пробы рантайма Lampa на ТВ).
🔴 **Права на файлы в `wwwroot/` — владелец `1000:1000` (= пользователь `lampac`
в контейнере), каталоги `755`, файлы `644`.** Не `kraken:kraken` — контейнер ходит
не под хост-пользователем. Если файл окажется `root:root 600`, dotnet ответит
`Permission denied`, а веб-сервер **молча отдаст 404** — отсутствие прав выглядит
снаружи как «файла нет».
### Раздача статики из `wwwroot/`
- Всё, что положено в `/lampac/wwwroot/`, доступно **по корню**:
`wwwroot/watchlist-films.json` → `GET /watchlist-films.json` (**200**).
- `UseStaticFiles` — в `Core.dll` (серверное ядро), настраивать не нужно.
- 🔴 **`/lampac/wwwroot` ≠ `/lampac/current.conf`**: статика ядра, а не модуль.
Проверять наличие файла надо `docker exec lampac ls ...`, а **доступность** —
обязательно через `curl`, потому что это разные вещи (см. права выше).
- ⚠️ Веб-корень отдаёт **только** `wwwroot/`. Пути вида `/cards/...` работают,
но `/card` — это **не** статика (см. ниже).
⚠️ Всё пишущее — на data-диск (1.8T). **НЕ на SD** (`/`, 14G, 8.6G свободно).
## docker-compose.yml (актуальный, 2026-09-22)
```yaml
services:
lampac:
image: ghcr.io/lampac-nextgen/lampac:latest
container_name: lampac
platform: linux/arm64
restart: unless-stopped
ports:
- "9118:9118"
environment:
- TZ=Asia/Bishkek
- LANG=ru_RU.UTF-8
volumes:
- ./config:/lampac/config
- ./cache:/lampac/cache
- ./data:/lampac/data
# Наш конфиг: перекрывает base.conf из образа. Включает модуль Catalog.
# Без монтирования правки теряются при пересоздании контейнера.
- ./config-module/init.conf:/lampac/init.conf:ro
# Каталоги Catalog
- ./catalog/sites:/lampac/module/Catalog/sites
- ./catalog/override:/lampac/module/Catalog/override
# Патч plugin.js. Файл живёт В ОБРАЗЕ — без монтирования патч теряется
# при обновлении образа. Нужен для видимости источника в UI (см. ниже).
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
# Статика: JSON-каталог watchlist (Lampac отдаёт wwwroot по HTTP сам)
- ./wwwroot:/lampac/wwwroot
```
⚠️ В контейнере рабочий каталог `/lampac`, часть файлов читается **из корня**, не из `config/`.
### Минимальный `config-module/init.conf`
Перекрывает `base.conf`, поэтому список `SkipModules` нужно **повторить целиком**, без `Catalog`:
```json
{
"BaseModule": {
"SkipModules": [
"Tracks", "Transcoding", "WebLog", "CacheMedia", "ForkPlayerXML",
"MsxNative", "Potok", "TelegramAuth", "TelegramAuthBot"
]
},
"LampaWeb": { "initPlugins": { "catalog": true } }
}
```
После любой правки — `docker compose up -d` (пересоздаст контейнер) или `docker restart lampac`.
## Модуль Catalog — внутренняя механика (разобрано по коду 2026-09-22)
Исходники внутри образа: `/lampac/module/Catalog/` —
`Controllers/{ApiController,ListController,CardController}.cs`, `ModInit.cs`, `plugin.js`, `sites/*.yaml`.
### Маршруты
| Роут | Назначение |
|---|---|
| `/catalog.js` | плагин для Lampa (клиентский JS) |
| `/catalog` | JSON: список каналов с их `menu` и `main` |
| `/catalog/list?plugin=
&cat=&page=N` | список элементов категории |
| `/catalog/card?plugin=&uri=&type=movie` | карточка фильма |
WAF на префиксе `/catalog/` — **10 запросов/сек**.
### 🔴 Питфолл №1: первая категория игнорирует `format`
`ListController.cs:50` строит базовый URL **до** обработки `format`:
```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
}
```
**Категория, стоящая ПЕРВОЙ в `menu.categories`, берёт базовый `url` из `list.firstpage`
и `format` для неё не применяется.** Симптом: первая категория отдаёт данные чужого файла
(например все 360 фильмов из общего `watchlist.json`), у элементов `img: null`,
`id` = ссылка на поиск Кинопоиска → в UI «здесь пусто».
**Правило:** `list.uri`/`firstpage` и первая категория в `menu.categories`
должны указывать на **один файл**. Переставляешь категории — правь `list.uri`.
### 🔴 Питфолл №2: `host` + `uri` склеиваются
Строка 50 склеивает `init.host` + `/` + `uri`. Значит `uri`/`firstpage` обязаны быть
**относительными** (`watchlist-films.json`), иначе двойной префикс:
`http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json` → пусто.
### 🔴 Питфолл №3: `main` в YAML запрещён
`CatalogSettings` не имеет поля `main` для входного YAML → YamlDotNet давится
неизвестным полем → `ModInit.goInit()` → `null` → `ApiController.cs:70` отбрасывает сайт:
```csharp
if (init == null || !init.enable || init.menu == null || init.hide) continue;
```
**В логах — тишина.** Сайт молча исчезает из `/catalog`. Обе формы (список и словарь) ломают.
`main` Catalog **генерирует сам** из `menu` — в JSON-ответе `/catalog` присутствуют оба
ключа с одинаковым содержимым. Писать `main` в YAML не нужно и нельзя.
⚠️ `menu` **обязателен** — он регистрирует канал, а не подменяет разделы.
### 🔴 Питфолл №4: элемент без `href` выбрасывается
```csharp
if (!string.IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href)) // ListController.cs
```
Элемент включается **только если есть и `name`, и `href`**. Симптом: из 404 позиций
отдалось 52 — ровно те, у кого есть ссылка. **Фикс:** генератор подставляет fallback
`https://www.kinopoisk.ru/index.php?kp_query=<название>`.
### Как `href` превращается в `id` (карточка фильма)
`ListController.cs:314–322`:
```csharp
else if (href.StartsWith("http"))
href = Regex.Replace(href, "https?://[^/]+/", ""); // вырезает домен
...
id = href,
```
Поэтому **полезно отдавать TMDB-URL**: `https://www.themoviedb.org/movie/687163` →
`id = "movie/687163"` → `CardController` идёт в TMDB и отдаёт полноценную карточку
(постер, жанры, описание, рекомендации). Работает для `movie/N` и `tv/N`.
`CardController` использует **встроенный TMDB-прокси Lampac**:
`http://{localhost}:{port}/tmdb/api/3/{cat}/{id}?api_key={CoreInit.conf.cub.api_key}`
— ключ уже внутри образа, отдельный TMDB-ключ для карточек не нужен.
### 🔴🔴 СЕДЬМАЯ СЕССИЯ: как РЕАЛЬНО внедрить секции в главную CUB (`partNext`)
**Каталог и карточки закрыты. Рабочий механизм секций найден: перехват `Lampa.Api.partNext`.**
#### Почему обёртка `Api.sources.cub.main` НЕ РАБОТАЕТ (three-strike)
Родной `main$1()` (@1257163 в `app.min.js`) объявлен **внутри замыкания Lampa** и опирается
на локальные `permit`, `get$7`, `Lang`, `permit.child_small`, `Helper$b`:
```js
function main$1() {
var parts_limit = 6;
var parts_data = [
function (call) { get$7('?sort=now_playing', params, function (json) {
json.title = Lang.translate('title_now_watch'); call(json); }, call, {life: day*2}); },
function (call) { if (permit.child_small) return call(); ... }
];
...
}
```
| Симптом | Замер CDP | Корень |
|---|---|---|
| `cub.main({})` → вечный `pending`, 0 fetch | `{"pending":true}` через 15 с | `permit` = **undefined** вне замыкания → падение на первой проверке → callback не зовётся |
| секции не добавлялись, ошибок нет | `{"cubMainSrc":"...Manifest.catalogs['watchlist']..."}` | `Manifest` / `Utils` / `network` = **undefined** вне замыкания → `ReferenceError` → `catch` → родной фид |
| «Нет подключения к сети» в логе ТВ | `{localhost}/catalog?uid=...&token={token}` | `Utils.account()` без `Manifest.api_host` подставляет `{localhost}` |
🔴 **Вывод: `cub.main` невызываем извне by design.** Обёртка — тупик, не «недоработка».
#### ✅ Правильный механизм: `Lampa.Api.partNext`
```js
// глобальная функция Lampa; CUB передаёт сюда parts_data — массив функций-секций
function partNext(parts, parts_limit, partLoaded, partEmpty) {
var pieces = parts.filter(function (p) { return typeof p == 'function'; })
.slice(0, 0 + parts_limit);
if (pieces.length) {
new Progress().append(pieces).start(function (result) {
var data = result.filter(function (r) { return r && r.results && r.results.length; });
...
});
} else partEmpty();
}
```
Патч добавляет свои функции-секции в `parts` **до** вызова оригинала:
```js
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);
};
```
Секция = функция `call(json)`, теми же контрактами, что родные CUB:
```js
function wlSection(name, url) {
return function (call) {
wlGet(url, function (json) {
json.title = name;
json.source = 'watchlist'; // ← обязательно, иначе карточка не найдётся
if (Lampa.Arrays.isArray(json.results))
json.results.forEach(function (it) { it.source = 'watchlist'; });
call(json);
}, function () { call(); }); // ошибка → пустой вызов (не ломает пул)
};
}
```
**Преимущество:** работает на **любой** странице, независимо от источника, без `permit`,
без `params`. Родной CUB не подменяется — секции добавляются в общий пул.
#### 🔴 Только ГЛОБАЛЬНЫЕ объекты — внутри замыкания `Manifest`/`Utils`/`network` нет
| Нужно | ❌ Не работает в этом контексте | ✅ Работает |
|---|---|---|
| api_host | `Manifest.api_host` | `Lampa.Manifest.api_host` (фолбэк — хардкод `http://192.168.1.14:9118`) |
| URL-склейка | `Utils.account(url)` → `{localhost}` | **ручная:** `wlAbs(url)` = host + path |
| HTTP | `network.silent(...)` | `new Lampa.Reguest().silent(absURL, ...)`, фолбэк `fetch` |
| список секций | `Manifest.catalogs['watchlist'].menu` | `window.__wl_menu` (заполняется в `loadCatalogs` из `/catalog`) |
```js
function wlAbs(url) {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
return wlHost() + (url.charAt(0) === '/' ? url : '/' + url);
}
```
#### Сборщик `build_plugin.py`
```bash
cd /Users/admin/lampac-tmp
python3 build_plugin.py # plugin-patched.js + INJECT → plugin-cub.js (14376 Б)
node --check plugin-cub.js
```
Детерминирован: режет `[до startPlugin] + INJECT + [новый startPlugin] + '})();'`,
проверяет инварианты (`INJECT=1`, `def=1`, `startPlugin=1`, `network.silent в INJECT=0`).
🔴 **Питфолл прошлого сборщика `patch_cub_inject.py`:** проверки `"X" not in s` шли
**по уже модифицированной строке** → повторный запуск вставлял INJECT дважды → плюс лишняя
`}` от вырезания → `SyntaxError: Unexpected token '}'` при балансе скобок `{: 123, }: 122`.
**Всегда проверять `node --check` + баланс скобок ПЕРЕД заливкой.**
#### 🔴 CDP-пробы на ТВ (порт 9998) — основной инструмент отладки
```bash
WS=$(curl -s "http://192.168.1.70:9998/json/list" \
| jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl')
python3 probe_ui.py "$WS"
```
Питон-клиент: `websocket-client` (`import websocket`). Модуль `websockets` **не установлен**.
`Runtime.evaluate` с `returnByValue: True, awaitPromise: True`.
| Скрипт | Назначение |
|---|---|
| `probe_ui.py` | **приёмка:** `Lampa.Router.call('main')` → чтение заголовков из DOM |
| `probe_inject.py` | инжект `plugin-cub.js` в рантайм **без деплоя** (быстрая итерация) |
| `probe_globals.py` | видимость `Manifest`/`Utils`/`network` |
| `probe_cub_main.py` | сколько секций отдаёт `cub.main` |
| `probe_wait.py` | ждёт результат + перехват `console.log` |
| `probe_tv.py`, `probe_cache.py`, `probe_calls.py` | состояние, кэш, перехват fetch |
🔴 **`eval()` внутри IIFE не создаёт глобальных функций.** После `eval(pluginCode)`
`wlHost()` снаружи → `ReferenceError: wlHost is not defined`. Это **нормально** —
`cub.main` их видит (одно замыкание), внешние пробы нет. Проверять только через
`Lampa.Api.main()` либо чтение DOM.
#### 🔴 Перезапуск Lampa на ТВ: `luna-send` требует файла, не inline-JSON
```bash
# ❌ печатает help: аргументы JSON не проходят через ssh + script -q -c
ssh root@192.168.1.70 'script -q -c "luna-send ... -a {\"id\":\"com.lampac.tv\"}" /dev/null'
# ✅ положить файл на ТВ и выполнить
scp tv_restart.sh root@192.168.1.70:/tmp/
ssh root@192.168.1.70 '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"}'
```
🔴 **`docker restart lampac` НЕ перезагружает плагин в памяти ТВ** (`catalog.js` кэширован
браузером webOS). Признак: `String(Lampa.Api.sources.cub.main)` содержит старый код.
Полный порядок деплоя: `scp plugin.js` → `chmod 644`/`chown 1000:1000` → `rm ._*` →
`docker restart lampac` (сброс `Staticache`) → **`sh /tmp/tv_restart.sh`** → `probe_ui.py`.
### 🔴 Питфолл №6: карточка без `card_parse` даёт HTTP 500
Без блока `card_parse` Catalog строит URL карточки как `$"{init.host}/{uri}"`
(`CardController.cs:33`) → `http://127.0.0.1:9118/movie/424781` — такого пути нет,
`html == null` → `BadRequest("html")` → **HTTP 500**, в UI «здесь пусто».
**Сигнатура роута (критично!):**
```csharp
[Route("catalog/card")]
public async Task Index(string plugin, string uri, string type)
```
🔴 **Параметра `cat` НЕТ.** Карточка знает только `uri` и `type`. Поэтому
`initUrl: $"…/cards-{cat}.json"` даёт `cards-.json` → 404 → HTTP 500.
**Что можно и что нельзя подставлять:**
| Поле | Механика | Подстановка |
|---|---|---|
| `initUrl` | `CSharpEval.Execute` с `CatalogInitUrlCard(host, args, uri, query, type)` | ✅ `uri`, `type`, `host` |
| `node` | `json.SelectToken(parse.node)` — чистый JSONPath | ❌ **статичен** |
| `name`/`image`/`description` | `ModInit.nodeValue(...)` → `attribute` | — |
**Питфолл JSONPath:** `SelectToken("movie/424781")` вернёт `null` — `/` в JSONPath это
**вложенность** (`movie` → `424781`). Симптом: **HTTP 400, тело `parse.node`**.
Слэш в ключе JSON недопустим для `SelectToken`.
**Рабочая схема (когда карточку отдаёт наш JSON, а не TMDB):**
```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--.json` (по одному на фильм) с обёрткой
`{"card": {...}}`. По одному файлу — потому что `node` статичен и один общий файл
по `uri` не отфильтровать.
**Диагностика (ошибки карточки в логи Lampac НЕ пишутся!):**
```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"
# тела-маркеры: "html" (URL не найден) | "parse.node" (node не нашёл узел) | "json" (не JSON)
```
### 🔴 Питфолл №5: кэш Catalog
Ключ: `catalog:{plugin}:{search}:{sort}:{cat}:{page}` — **не зависит от содержимого файлов**.
`hybridCache` живёт **в памяти процесса** (не в `cache/fdb/`, не сбрасывается рестартом
контейнера — проверено). После перегенерации JSON выдача может не меняться, пока
ключ тот же. Приём `?nocache={Guid.NewGuid()}` в `format` **не помогает**.
Практический вывод: при отладке менять сам **URL/ключ** (другой `cat` или `sort`),
а не надеяться на инвалидацию кэша.
### `plugin.js` — клиентская часть
Лежит **в образе** (`/lampac/module/Catalog/plugin.js`), не на HDD. Что делает:
- `main()` — итерирует `catalog.main`, собирает секции **СВЕРХУ** родного каталога.
🔴 **НО:** работает это только при выбранном нашем источнике, потому что `Api.getCatalog()`
читает манифест **текущего** источника. Для секций поверх CUB нужен перехват
`Lampa.Api.partNext` — см. «СЕДЬМАЯ СЕССИЯ» выше.
- `menu()` — итерирует `catalog.menu` из YAML
- `full()`/`list()`/`search()` — карточка, категория, поиск
- `Lampa.Listener.follow('menu', ...)` + `e.abort()` — **перехват кликов** по родным
разделам (`movie`/`tv`/`cartoon`/`anime`/`relise`). Срабатывает **только когда
выбран источник «Мой список»**, поэтому родной CUB не трогается при других источниках
- `loadCatalogs()` — сохраняет `window.__wl_menu` из `/catalog` (нужно патчу `partNext`)
### 🔴 Питфолл №6: источник не виден в UI (`enumerable: false`)
```js
Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; } });
```
Свойство создаётся **не-enumerable** → `Object.keys(Lampa.Api.sources)` его не видит →
Lampa не рисует источник в панели выбора. При этом `Api.sources['Мой список']`
доступен напрямую и каталог отдаёт. Замер через DevTools ТВ
(`http://192.168.1.70:9998/json`) → `{"enumerable":false,"configurable":false}`.
**Фикс:** добавить `enumerable:true,configurable:true` в **оба** вхождения. Правка
применяется скриптом `patch_plugin.py` (не sed/grep), проверка синтаксиса —
`node --check` перед заливкой. Монтируется через compose (см. выше).
### Сканирование `sites/`
`Path.Combine(AppContext.BaseDirectory, ModInit.modpath, "sites")` — папка сканируется
на старте контейнера. Кэш-фикс: `docker restart lampac` (старый подход) **не всегда
достаточен** — см. питфолл №5.
## Управление
```bash
ssh kraken
cd /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac
docker compose up -d
docker compose logs -f
docker compose restart
docker compose down
```
Правка `docker-compose.yml` — без `sudo` (каталог `kraken:kraken`).
Создание новых каталогов в корне диска — **через `sudo`** (корень `root:root`). У `kraken` sudo без пароля.
### Развёртывание каталога watchlist (`deploy_lampac.py`)
Локальный скрипт `/Users/admin/lampac-tmp/deploy_lampac.py` — 7 шагов с бэкапами
и самопроверкой. Запуск:
```bash
python3 /Users/admin/lampac-tmp/deploy_lampac.py # полный цикл
python3 /Users/admin/lampac-tmp/deploy_lampac.py --skip-plugin # только JSON+YAML
```
Порядок: бэкап JSON в `wwwroot/.bak/` → заливка 5 JSON → `watchlist.yaml` →
патченный `plugin.js` → volume в compose (если нет) → `docker compose up -d` →
проверка (`/catalog`, 4 категории с подсчётом постеров и TMDB-ID, факт патча).
### Генератор каталога (`gen_watchlist_v2.py`)
`/Users/admin/lampac-tmp/gen_watchlist_v2.py `
Читает `.md`, резолвит каждый фильм: **сначала Radarr** (мгновенно, там уже `tmdbId`
и постер), **затем TMDB** по названию (2 языка × movie/tv), **фолбэк** — ссылка Кинопоиска.
Кэш `~/.lampac_tmdb_cache.json`, ключ `title|year`.
Первый прогон: **330 постеров из 403** (Radarr 7, TMDB 327, фолбэк 69).
Занимает **8–10 минут** (403 фильма × TMDB, `sleep(0.02)`).
🔴 **Запускать в background** — foreground упрётся в лимит 600 с.
Требует `TMDB_KEY` из `/Users/admin/Developer/watchlist-sync/.env`.
⚠️ TMDB-хук `api.themoviedb.org` с Кра́кена доступен (проверено: 401 без ключа = сеть есть).
## Проверка работоспособности
```bash
curl -sS --max-time 20 "http://localhost:9118/version?type=hash"
# → 46b29e1fe8fa6021cd83e75c41abffb7 (для текущего :latest)
```
По сети: `http://192.168.1.14:9118/version?type=hash` — то же значение.
## ⚠️ Питфоллы
### 1. Первый старт долгий (~4 минуты)
Контейнер компилирует и грузит ~40 native-модулей:
`compilation LampaWeb`, `DLNA`, `GStreamer`, `Sync`, `TorrServer`, ... затем `loaded module: <имя>`.
Пока идёт загрузка, HTTP **рвёт соединение**:
```
curl: (56) Recv failure: Connection reset by peer
```
🔴 **`health: healthy` не означает готовность HTTP.** Статус встаёт раньше.
Признак готовности — лог **`Configure complete`** (идёт после последнего `loaded module:`).
**Не перезапускать контейнер, не считать это поломкой.** Просто ждать.
### 2. `docker manifest inspect` с Mac отдаёт `denied`
GHCR требует авторизацию при обращении с Mac. Проверять наличие `linux/arm64` **из Кра́кена**:
```bash
ssh kraken 'docker manifest inspect ghcr.io/lampac-nextgen/lampac:latest' | jq -r '.manifests[].platform | .os + "/" + .architecture'
# linux/amd64
# linux/arm64 ← есть, сборка из исходников не нужна
# unknown/unknown
# unknown/unknown
```
### 3. Шаблон логов первого старта
```
compilation Alloha
compilation LampaWeb
...
loaded module: Vibix
loaded module: TorrServer
...
Configure complete ← HTTP начинает отвечать
```
### 4. 🔴 `sudo` через `ssh` без tty падает
```bash
ssh kraken "sudo docker exec lampac ls /lampac"
# sudo: a terminal is required to read the password;
# either use the -S option ... or configure an askpass helper
```
**Фикс: флаг `-t`.** Но ⚠️ при `-t` **нельзя передавать `a && b`** — цепочка команд
рвёт tty-сессию (`Connection to kraken closed`), и первая же команда отваливается.
**Каждую команду вызывать отдельно:**
```python
def sh(cmd):
return subprocess.run(f"ssh -t kraken {cmd}", shell=True, ...)
sh("sudo cp /tmp/x.json /srv/.../wwwroot/x.json") # ✅ одна команда
sh("sudo chown kraken:kraken /srv/.../wwwroot/x.json")
sh("sudo chmod 644 /srv/.../wwwroot/x.json")
# ❌ sh("sudo cp … && sudo chown … && sudo chmod …") — рвёт tty
```
### 5. 🔴 `docker compose up -d` не пересоздаёт контейнер при смене volumes
Изменение **только списка `volumes`** в `docker-compose.yml` не приводит к пересозданию:
`compose up -d` считает конфигурацию контейнера неизменной. Симптом: новый bind-mount
не появился, патч `plugin.js` не виден внутри.
**Проверка:**
```bash
ssh kraken "sudo docker inspect lampac --format '{{json .Mounts}}'" | jq -r '.[].Destination'
```
**Фикс:**
```bash
ssh kraken "cd /srv/dev-disk-by-uuid-6194539b-.../lampac && sudo docker compose up -d --force-recreate"
```
## Сеть
| Хост | IP |
|---|---|
| Кра́кен (eth0) | `192.168.1.14` (второй адрес `192.168.1.15`) |
| ТВ LG | `192.168.1.70` |
| Mac Alex | `192.168.1.57` |
Один сегмент `192.168.1.x` — туннели и проброс портов не нужны.
Подробнее: [[family/how-to/kraken-network]].
## Модуль Catalog — каталоги из YAML
Модуль **`Catalog`** (в образе, по умолчанию **выключен**) отдаёт свои каталоги как плагин Lampa.
Разобрано по коду и **включено в работу 2026-09-22** — см. [[personal/plans/lampac-watchlist-plugin]].
### Наш каталог «Мой список»
Отдаёт `movies-watchlist.md` как каталог в Lampa. Файлы:
| Путь на HDD (`.../lampac/`) | Роль |
|---|---|
| `config-module/init.conf` | включает Catalog (монтируется в `/lampac/init.conf:ro`) |
| `catalog/sites/watchlist.yaml` | описание каталога для Catalog |
| `catalog/override/` | переопределения (пусто) |
| `wwwroot/watchlist.json` + `watchlist-.json` | JSON-данные (Lampac отдаёт по HTTP) |
| `scripts/gen_watchlist.py` | генератор: .md → JSON |
| `~/scripts/watchlist-catalog.sh` | обёртка + перезапуск (сброс кэша), cron 02:30 |
Плагин на ТВ: `http://192.168.1.14:9118/catalog.js` → источник «Мой список».
⏳ **Переделывается (2026-09-22).** Текущая реализация признана неверной: заполнен
блок `menu` (подменяет родной каталог CUB вместо добавления секций сверху), а данные
берутся парсингом `.md` вместо **Radarr API** (в `.md` нет TMDB ID и постеров).
Разбор ошибки — [[personal/plans/lampac-watchlist-plugin]], секция «🔴 АРХИТЕКТУРНАЯ ОШИБКА».
### 🔴 Рабочая форма `sites/watchlist.yaml` (проверена 2026-09-22, откат после провала)
```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 }
```
### 🔴 `main` в YAML ЗАПРЕЩЁН — ломает канал (опровергнуто 2026-09-22)
**Раздел ниже (гипотеза «`menu` подменяет / `main` добавляет») — НЕВЕРЕН.** Проверено
на живом сервере: попытка добавить блок `main` в YAML → **«Мой список» исчез из `/catalog`**,
`catalog/list?plugin=watchlist` отдавал пустоту. **Три раунда правок, три рестарта.**
| Гипотеза (неверна) | Факт (проверено) |
|---|---|
| `menu` подменяет родные разделы | ❌ `menu` **регистрирует канал**. `ApiController.cs:70`: `if (init == null \|\| !init.enable \|\| init.menu == null \|\| init.hide) continue;` — без `menu` сайт **отбрасывается** |
| `main` добавляет секции сверху | ❌ блока `main` **не существует** во входном YAML. YamlDotNet давится неизвестным полем → `ModInit.goInit(site)` → **null** → сайт молча пропадает |
| Заполнять нужно только `main` | ❌ `main` Catalog **генерирует САМ** из `menu`. В JSON-ответе `/catalog` есть **оба** ключа с одинаковым содержимым |
⚠️ **Обе формы `main` ломают одинаково** — и список, и словарь:
```yaml
main: # вариант 1 — список: НЕ работает
- categories:
Фильмы: films
```
```yaml
main: # вариант 2 — словарь: НЕ работает
Фильмы: http://...
```
⚠️ **Ошибка не логируется.** В логах Lampac — тишина (`docker logs lampac | grep -i yaml`
пусто). Сайт просто не появляется в `/catalog`. Диагностика только перебором YAML.
**Кто на самом деле подменяет родные разделы — `plugin.js`, а не YAML:**
```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
не трогается, пока источник не переключён на наш.
### 🔴 `host` + `uri` СКЛЕИВАЮТСЯ — `uri` обязан быть относительным
`ListController.cs:53`:
```csharp
string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
? init.list?.firstpage : init.list?.uri)}";
```
Полный URL в `uri`/`firstpage` даёт **двойной префикс** → пусто:
```
http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json
```
Правильно: только `watchlist-films.json`, `host` приклеится сам. Так же устроены
родные YAML (`vokino.yaml` → `uri: search?name={search}`).
### ⚠️ Питфолл: явный `format` в `menu` перезагружает страницу
С `format`-блоком в `menu` Lampa **перезагружает прогрузку страницы при каждом выборе
раздела** (наблюдение Alex). Если перезагрузка не нужна — убрать `format` из `menu`,
оставить только `categories`.
### 🎯 Как добиться «CUB + мой список сверху» — ✅ РЕШЕНО 2026-09-22
Родной CUB и «Мой список» — это **два источника Lampa**, переключаются в
«остальное → основной источник». Одновременно показать нельзя без вмешательства:
| Путь | Реализуемость |
|---|---|
| Блок `main` в YAML | ❌ **невозможно** — ломает парсинг |
| Оставить `menu` как есть | ⚠️ источник работает, но разделы подменяются, пока он выбран |
| Патч `plugin.js` — обёртка `cub.main` | ❌ **тупик:** `cub.main` невызываем извне (`permit` в замыкании) |
| **Патч `plugin.js` — перехват `Lampa.Api.partNext`** | ✅ **РАБОТАЕТ** — проверено на живом ТВ через CDP |
✅ **Итоговое решение — `wlPatchPartNext()`.** `partNext` глобальный, принимает пул
функций-секций; наши добавляются в тот же пул, что и родные CUB. Работает на любой
странице, независимо от источника. Реализация, питфоллы и probe-скрипты —
раздел **«🔴🔴 СЕДЬМАЯ СЕССИЯ: как РЕАЛЬНО внедрить секции в главную CUB»** выше.
### ⚠️ Баг Lampac: источник не виден в списке выбора
`plugin.js`, строка 59 (`loadCatalogs()`), **два вхождения**:
```js
Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; } });
```
`defineProperty` по умолчанию `enumerable: false` → `Object.keys(Lampa.Api.sources)`
не перечисляет источник → Lampa не рисует его в панели выбора. При этом
`Api.sources['Мой список']` работает напрямую и каталог отдаёт.
**Фикс:** `{ get: ..., enumerable: true, configurable: true }`.
`plugin.js` живёт **в образе**, не на HDD (`docker inspect` — монтируются только
`catalog/sites` и `catalog/override`). Правка через `docker exec` теряется при
пересоздании. Постоянный путь — volume:
```yaml
volumes:
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
```
Проверка синтаксиса ПЕРЕД заливкой: `node --check /tmp/catalog-plugin.js`.
Статус: **не применено**, ждёт апрува Alex.
### Radarr как источник данных (постеры, TMDB ID) — ✅ ПРОВЕРЕНО 2026-09-22
Фильмы уже резолвлены в Radarr — там есть TMDB ID и постеры. Это **правильный
источник** для каталога; `.md` их не содержит.
```bash
# API-ключ Radarr
grep -oE "[^<]+" \
/srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config/config.xml
# → cbcb8ec320104359920c0f611d503c7b
# ⚠️ -L обязателен, иначе 307 и пустое тело
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # → 50
# Sonarr, порт 8989, ключ 15fbec32f1cb4b05b1c2b229ce27359b
curl -sL "http://localhost:8989/api/v3/series?apikey=$SKEY" | jq length # → 13
```
**Поля ответа Radarr (для каталога нужны все):**
`tmdbId`, `imdbId`, `title`, `originalTitle`, `year`, `titleSlug`, `hasFile`,
`genres`, `ratings.tmdb.value`, `images[] | select(.coverType=="poster") | .remoteUrl`.
Кэш постеров на диске: Radarr `/config/MediaCover/` (46 каталогов).
```json
{
"tmdbId": 1292695, "imdbId": "tt31728330",
"title": "They Will Kill You", "year": 2026,
"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`), уже включён.
⚠️ **Связки kp ↔ tmdb в данных нет.** В `.md` — 68 ссылок `kinopoisk.ru/film//`,
в Radarr — `tmdbId`. Прямого соответствия нет ни в одном файле. **Мост — модуль
`watchlist_sync/resolver.py`** (см. ниже).
### `watchlist_sync/resolver.py` — резолвер постеров (уже написан!)
Путь: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync/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` — ИСТОЧНИК АВТОСКАЧКИ (в коде!)
```python
# radarr_add():
"monitored": True,
"addOptions": {"searchForMovie": True}, # ← качает СРАЗУ после добавления
# sonarr_add():
"monitored": True,
"addOptions": {"searchForMissingEpisodes": True}, # ← то же для сериалов
```
⚠️ **Даже если снять `monitored` вручную, следующий запуск добавления из списка
включит автоскачку снова** — флаг зашит в код. Фикс: `searchForMovie` → `False`,
`searchForMissingEpisodes` → `False`. **Статус: не применён, на апруве.**
### Как выключить автоскачку на HDD — ✅ ВЫПОЛНЕНО 2026-09-22
| Сервис | Очередь | `monitored` | `rssSyncInterval` |
|---|---|---|---|
| **Radarr** | 6 → **0** | 45 → **0** | 30 → **0** |
| **Sonarr** | 19 → **0** | 13 → **0** | 15 → **0** |
Метаданные **целы**: 50 фильмов, 46 с постерами, 50 с `tmdbId` и рейтингом,
24 файла на диске **не тронуты**. Бэкапы — метка `20260922-123404`.
**Проверка пассивности:** Transmission за 45 с — NET `290GB/26.7GB`, BLOCK `214GB/393kB`
не сдвинулись. Логи Radarr (`search|download|grab`) за 2 мин — пусто.
**Правильные эндпоинты (проверено):**
```bash
# отмена загрузки — файлы на диске НЕ трогаются
DELETE /api/v3/queue/?removeFromClient=true&blocklist=false
# снятие monitored — КЛЮЧ ПОЛЯ РАЗНЫЙ!
PUT /api/v3/movie/editor {"movieIds": [...], "monitored": false} # Radarr
PUT /api/v3/series/editor {"seriesIds": [...], "monitored": false} # Sonarr, НЕ "ids"
# RSS off — отправлять ВЕСЬ объект конфига с id, иначе молча вернёт старое
PUT /api/v3/config/indexer {"rssSyncInterval": 0, "id": 1, ...остальное}
```
**Откат:**
```bash
curl -sL -X PUT ".../api/v3/movie/editor" -d '{"movieIds":[...],"monitored":true}'
curl -sL -X PUT ".../api/v3/series/editor" -d '{"seriesIds":[...],"monitored":true}'
curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1,...}'
cp radarr.db.bak-20260922-123404 radarr.db && docker restart radarr
```
### 🔴 Питфолл: `urllib` не идёт по 307, `curl -sL` идёт
Симптом: **все** DELETE/PUT из Python-скриптов вернули
`HTTP Error 307: Temporary Redirect`, **ничего не изменилось**.
Штатный `urllib` на 307/308 превращает DELETE в GET и теряет тело.
**Фикс — свой redirect-хендлер:**
```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)
_OPENER = urllib.request.build_opener(RedirectKeepMethod)
# далее _OPENER.open(req) вместо urllib.request.urlopen(req)
```
Прочие грабли сессии:
- **Sonarr группирует эпизоды сезона под одним `id`** — после удаления первой записи
остальные дают `404`, это **не сбой**, они ушли с родительской.
- **Кириллица внутри inline-`jq` в `ssh '...'`** ломает команду
(`unexpected INVALID_CHARACTER`). Ключи объекта — латиницей: `total`, `monitored`, `queue`.
## TorrServer внутри Lampac — на HDD НЕ пишет (проверено 2026-09-22)
| Параметр | Значение | Смысл |
|---|---|---|
| `UseDisk` | **`false`** | 🔴 **на диск не пишет вообще** |
| `CacheSize` | 100663296 (96 МБ) | кэш в RAM |
| `TorrentsSavePath` | `""` | пути сохранения нет |
| `TorrentDisconnectTimeout` | 180 | раздача живёт 3 мин после остановки |
```bash
# конфиг TorrServer
docker exec lampac cat /lampac/data/ts/settings.json | jq '.BitTorr | {UseDisk, CacheSize, TorrentsSavePath}'
# жив ли
curl -s http://192.168.1.14:9118/ts/echo # → MatriX.135
curl -s -X POST http://192.168.1.14:9118/ts/torrents \
-H "Content-Type: application/json" -d '{"action":"list"}' # → []
```
## 🔴 Поиск торрентов: отдельного Jackett НЕТ, его роль играет Lampac
`lampainit.js` прописывает:
```js
Lampa.Storage.set('parser_torrent_type', 'jackett');
Lampa.Storage.set('jackett_url', '192.168.1.14:9118'); // ← сам Lampac!
Lampa.Storage.set('jackett_key', '1');
```
Контейнера **Jackett в системе нет**. Lampac отдаёт Jackett-совместимый API сам:
```bash
# ⚠️ на КОРНЕ, БЕЗ префикса /jackett (с ним → 404)
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 ставить НЕ нужно** — так задумано производителем.
## Занятые порты на Кра́кене (2026-09-22)
`7878` Radarr · `8096` Jellyfin · `8191` FlareSolverr · `8989` Sonarr · `9000` Portainer ·
`9091` Transmission · `9696` Prowlarr · `51413` Transmission (UDP) · **`9118` Lampac**
## Отладка UI Lampa на ТВ (webOS)
Правки по SSH не сделать, но **состояние читается** через Chrome DevTools Protocol:
```bash
# webOS отдаёт DevTools на порту 9998 (WebAppMgr --remote-debugging-port=9998)
curl -s http://192.168.1.70:9998/json | jq -r '.[] | "\(.type): \(.url)"'
# → file:///.../com.lampac.tv/index.html?card=...&source=cub
```
Дальше `websocket` + `Runtime.evaluate` читает живые переменные:
| Выражение | Что даёт |
|---|---|
| `Lampa.Storage.field('source')` | выбранный источник |
| `Object.keys(Lampa.Api.sources).join('\|')` | перечисляемые источники |
| `Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список')` | `enumerable`/`configurable` |
| `Lampa.Api.sources['Мой список'].getCatalog()` | ключи каталога |
Скрипты-пробы: `/tmp/lampa_probe.py`, `/tmp/lampa_probe2.py`, `/tmp/lampa_probe3.py`.
⚠️ `tcpdump` на Кра́кене **нет**. `luna://com.webos.service.capture/takeScreenShot`
на этом ТВ **не существует**. `luna-send` пишет в tty → нужен `script -q -c`.
Подробности: [[family/how-to/tv-lampac-plugin]].
### Общее про модуль
| Что | Как |
|---|---|
| Данные | `sites/.yaml` — `host`, `menu`, `search`, `list`, `content` |
| Переопределения | `override/.yaml`, `override/_.yaml` |
| Роуты | `/catalog.js`, `/catalog`, `/catalog/list`, `/catalog/card` |
| WAF | `^/catalog/` → 10 req/sec |
| Примеры | 10 файлов: `vokino`, `rezka`, `kinopub`, `aniliberty`, `eneyida`, `filmix`, `filmixapp`, `getstv`, `kinobase`, `kinoukr` |
**Обязателен HTTP:** `if (!url.StartsWith("http")) url = $"{init.host}/{url}"` — локальные файлы не читаются.
**Два режима:** `jsonPath: true` → `JToken.Parse` (JSON), иначе HtmlAgilityPack (HTML).
🔴 **Питфолл: элемент с пустым `href` ВЫБРАСЫВАЕТСЯ** —
`if (!IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href))` (ListController.cs).
Нужен непустой `href` у каждого элемента.
🔴 **Питфолл: `{cat}` НЕ подставляется в `nodes`** — JSONPath статичен.
Разные категории = разные JSON-файлы.
🔴 **Питфолл: кэш без версии данных** — ключ `catalog:{plugin}:{search}:{sort}:{cat}:{page}`.
После правки данных нужен `docker restart lampac`.
**Выключен в двух местах** `base.conf`/`current.conf`:
```jsonc
"BaseModule": { "SkipModules": [ "Catalog", ... ] }
"LampaWeb": { "initPlugins": { "catalog": false } }
```
Синапс модулей (всего 33): `AdminPanel, Adult, Catalog, Community, DLNA, DatabaseEditor, ExternalBind,
ForkPlayerXML, GStreamer, JacRed, Kit, LampaWeb, LampacApk, LogUserRequest-Lite, MsxNative, Music,
NextHUB, Online, OnlineAnime, OnlineENG, OnlineGEO, OnlinePaid, OnlineRUS, OnlineUKR, PidTor, Potok,
Proxy, SISI, Sync, Telemetry, Tracks, Transcoding, WebLog`.
Полный пример YAML (`vokino.yaml`): `docker exec lampac cat /lampac/module/Catalog/sites/vokino.yaml`.
Выбор порта — `docker compose config` + `sudo ss -tlnp` перед деплоем.
## Интеграция с Lampa (ТВ) — ✅ СДЕЛАНО 2026-09-22
- Плагин: `http://192.168.1.14:9118/online.js`
- TorrServer в Lampa: Настройки → TorrServer → «Использовать основную ссылку» → `http://192.168.1.14:9118`
- Буфер плеера: 50–100 МБ
- `/lg.ipk` отдаёт сам Lampac (проверено: HTTP **200**)
### Установка клиента на LG webOS — по SSH, без флешки
🔴 **`Media Station X` и `.ipk` с флешки — НЕ нужны.** ТВ имеет SSH-доступ (root), установка делается с Mac.
**ТВ:** `root@192.168.1.70`, ключ `~/.ssh/konka_webos`, webOS **5.4.2** (ядро 4.4.84, image `starfish-dvb-secured`, build 2602, `webos_build_datetime 20230405092501`).
Порты открыты: `22` (SSH), `3000`/`3001` (Luna). Из кастомного уже стоит: `org.webosbrew.hbchannel` 0.7.2, `org.webosbrew.inputhook`, `me.wouterdek.magic4pc`.
**Порядок установки:**
```bash
# 1. Скачать .ipk от Lampac прямо на ТВ
ssh root@192.168.1.70 'curl -sS --max-time 60 -o /tmp/lampa.ipk "http://192.168.1.14:9118/lg.ipk"'
# → 6 896 874 байт, формат ar (!), НЕ tar/gzip
# 2. Установить (обязательна обёртка script — см. питфолл ниже)
ssh root@192.168.1.70 'rm -f /tmp/inst.txt; script -q -c \
"luna-send -n 1 -f luna://com.webos.appInstallService/dev/install \
{\"id\":\"org.lampa.tv\",\"ipkUrl\":\"/tmp/lampa.ipk\",\"subscribe\":true}" \
/tmp/inst.txt >/dev/null 2>&1; cat /tmp/inst.txt'
# → {"subscribed":true,"returnValue":true}
# 3. Проверить, что установилось
ssh root@192.168.1.70 'ls /media/developer/apps/usr/palm/applications/'
# → com.lampac.tv ← имя приложения Lampa, НЕ org.lampa.tv
# 4. Запустить
ssh root@192.168.1.70 'rm -f /tmp/l.txt; script -q -c \
"luna-send -n 1 -f luna://com.webos.applicationManager/launch {\"id\":\"com.lampac.tv\"}" /tmp/l.txt'
# 5. Убедиться, что открылось
ssh root@192.168.1.70 'rm -f /tmp/f.txt; script -q -c \
"luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}" /tmp/f.txt; grep appId /tmp/f.txt'
# → "appId": "com.lampac.tv"
```
**Фактический ID приложения — `com.lampac.tv`** (vendor Lampac, version 1.0.0, type web).
⚠️ В `luna-send ... dev/install` поле `"id"` — **произвольная метка**; реальный appId берётся из `appinfo.json` внутри ipk. Ошибиться здесь не страшно, но искать приложение надо по фактическому ID.
### 🔴 Питфолл: `luna-send` по SSH молчит
**Симптом:** любой вызов `luna-send` через `ssh root@TV "luna-send ..."` печатает **хелп-экран**, а не результат. С `-t` (PTY) — тоже. С `-a '{"json"}'` — тоже.
**Причина:** `luna-send` детектирует TTY и отказывается писать в pipe.
**Решение:** обернуть в `script(1)`, который аллоцирует псевдо-TTY, и прочитать временный файл:
```bash
ssh root@TV 'script -q -c "luna-send -n 1 -f luna://service/method {json}" /tmp/o.txt; cat /tmp/o.txt'
```
Готовый враппер: `~/Automation/tv-luna-send.sh` (`TV_HOST` переопределяет таргет).
Доки: [[family/how-to/tv-luna-send]], [[family/how-to/htpc-webos-luna]].
⚠️ **Установка идёт через `com.webos.appInstallService/dev/install`**, а НЕ через
`org.webosbrew.hbchannel.service/install` (последний в образе есть, но `luna-send` его не дёрнул чисто).
⚠️ `luna-send -i` (subscribe, бесконечный) через враппер **не работает** — нужен polling.
### Первый просмотр — проверено 2026-09-22
Фильм запущен, **не дёргается**. Alex: «Вроде стабильно, не делается».
| Факт | Значение |
|---|---|
| Соединения ТВ → Lampac | 3 × ESTABLISHED `192.168.1.70 → 192.168.1.14:9118` |
| Нагрузка контейнера | `lampac` CPU 57%, NET 168 МБ ↓ / 132 МБ ↑ |
| Битрейт на eth0 | **~3–4 Мбит/с** |
⚠️ **Почему всего 3–4 Мбит, а не 15:** выбран источник **«Смотреть онлайн (lampac 1.18…)»** — встроенный
плагин `online.js`, HLS-поток. Lampac отдаёт **ссылку**, ТВ тянет видео напрямую из CDN (в `/proc/net/tcp`
ТВ видны внешние адреса на `:443`/`:8443`). Тяжёлый remux с торрента не участвует — поэтому и не дёргается.
🔴 **Следствие:** этот просмотр **не проверяет**, держит ли сеть remux 60–80 Мбит.
Тест функциональности, не полосы. Полосу проверять на торрент-источнике.
**Как убрать выбор источника:** Lampa → Настройки → Плагины → выключить `online.js`. Останутся только торренты.
## 🔴 Выбор источника каталога в Lampa (проверено 2026-09-22)
**В Lampa НЕТ пункта «Настройки → Источник».** Я его выдумал — Alex дважды указал на ошибку.
Не повторять.
Источник (Rezka / Kinobase / «Мой список» / …) переключается **из меню каталогов на главной**:
1. Открыть Lampa на главной
2. Встать на любой ряд каталога → нажать **влево** на пульте
3. Слева выедет **панель со списком источников**
4. Выбрать нужный → OK; каталог перестроится, в заголовке появится имя источника
в верхнем регистре («Фильмы — МОЙ СПИСОК»)
**Плагин `catalog.js` не добавляет пункт меню — он подменяет существующие.**
Механика: при старте приложения `Lampa.Utils.account(api_host + '/catalog', …)` получает список
каталогов, регистрирует каждый как `Lampa.Params.values.source[key]` и подменяет
`Lampa.Api.sources[key]`. Затем хук `Lampa.Listener.follow('menu', …)` перехватывает клики по
`movie`/`tv`/`cartoon`/`anime`/`relise` и подставляет URL из `catalogs[key].menu`.
⚠️ **Следствие — каталоги регистрируются ТОЛЬКО при старте приложения.**
Добавил плагин и не перезапустил Lampa → источника не будет, `/catalog` не запрашивается,
соединений с `:9118` ноль. Нужен **полный перезапуск** (не сворачивание).
### ⚠️ Клиент на ТВ — это не «отдельная Lampa»
`com.lampac.tv` (webOS) грузит UI **с сервера**: `loader.js` читает `lampac_url` из localStorage
(дефолт `http://192.168.1.14:9118`) и подключает `app.min.js` + `lampainit.js`.
Значит логика каталога на ТВ = **серверная**, и править её надо на Lampac, а не на ТВ.
`/lampainit.js` содержит `var plugins_add = [...]` со строкой
`{"url":".../catalog.js","name":"Альтернативные источники каталога","status":1}` —
это **серверный список плагинов, которые Lampac сам подсовывает** в UI.
Каталог watchlist работает через **модуль `Catalog`**, не через эту строку.
Не искать причину поломки в `plugins_add`.
### 🔴 БАГ Lampac: источник не виден в панели (`enumerable: false`)
**Найдено 2026-09-22** диагностикой через Chrome DevTools ТВ. Симптом Alex:
«Ни на главной ни в Фильмах нет! Там только Сейчас смотрят, рекомендуем итд».
**Факты с живого ТВ:**
| Замер | Значение |
|---|---|
| `Lampa.Storage.field('source')` | `'Мой список'` — источник **уже выбран** |
| `Object.keys(Lampa.Api.sources)` | `'tmdb|cub'` — ❌ источника нет в перечислении |
| `Lampa.Api.sources['Мой список'].getCatalog()` | `search, movie, cartoons, anime, menu, main` — ✅ работает |
| `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; } });
// НАДО
Object.defineProperty(Lampa.Api.sources, key,
{ get: function(){ return Api; }, enumerable: true, configurable: true });
```
Не-enumerable свойство не попадает в `Object.keys()` → Lampa не рисует источник в панели
выбора. **Баг Lampac, не нашей настройки.**
🔴 **`plugin.js` живёт в ОБРАЗЕ, не на HDD.** `docker inspect` показывает монтирования
только на `catalog/sites` и `catalog/override`. Правка через `docker exec` **исчезнет
при обновлении образа** — нужен bind-mount:
```yaml
# docker-compose.yml
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
```
```bash
ssh kraken 'docker exec lampac cat /lampac/module/Catalog/plugin.js' > /tmp/catalog-plugin.js
cp /tmp/catalog-plugin.js /tmp/catalog-plugin.js.bak-$(date +%Y%m%d-%H%M%S)
# правка двух вхождений defineProperty (только через patch, не sed)
node --check /tmp/catalog-plugin.js # проверить синтаксис ДО заливки
scp /tmp/catalog-plugin.js kraken:/srv/.../lampac/catalog/plugin.js
ssh kraken 'cd /srv/.../lampac && docker compose up -d'
```
**Статус: фикс НЕ применён**, ждёт апрува Alex (2026-09-22).
Полный разбор — [[personal/plans/lampac-watchlist-plugin]], раздел «ГЛАВНЫЙ ПИТФОЛЛ».
### 🛠 Диагностика ТВ через Chrome DevTools — порт 9998
**Главный инструмент для отладки UI Lampa.** webOS отдаёт Chrome DevTools Protocol:
у процессов `WebAppMgr` есть `--remote-debugging-port=9998`. Даёт **чтение состояния
живой Lampa** — ни логи, ни tcpdump этого не дают.
```bash
# цель отладки + URL приложения
curl -s --max-time 8 "http://192.168.1.70:9998/json" | /usr/bin/jq -r '.[] | "\(.type): \(.url)"'
# → page: file:///media/developer/apps/usr/palm/applications/com.lampac.tv/index.html?...
```
Чтение переменных — `websocket-client` + `Runtime.evaluate`:
```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)
# Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список')
```
Готовые скрипты: `/tmp/lampa_probe.py` (состояние источника),
`/tmp/lampa_probe3.py` (асинхронный вызов `menu()` каталога).
**Ловушки:**
⚠️ **Замер соединений — на ТВ, не на сервере.** `sudo ss -tn | grep :9118` на Кра́кене
сразу после старта Lampa даёт **0** (ложный вывод «не ходит»). Через ~30 с:
`ssh root@192.168.1.70 'netstat -tn | grep 9118'` → **1 ESTABLISHED + 6 TIME_WAIT**.
⚠️ **`launch` через luna требует экранированного JSON в двойных кавычках**, иначе
`errorCode: 2, invalid parameters`. `getForegroundAppInfo {}` работает без кавычек.
⚠️ **`tcpdump` на Кра́кене нет** — не ставили, DevTools дал больше.
⚠️ **`luna://com.webos.service.capture/takeScreenShot` не существует** на этом ТВ
(`Unknown method`) — скриншот так не снять.
### Диагностика «каталога нет в UI»
| Проверка | Команда | Норма |
|---|---|---|
| Плагин отдаётся | `curl -sI http://192.168.1.14:9118/catalog.js` | 200, ~32 КБ |
| Каталоги зарегистрированы | `curl -s http://192.168.1.14:9118/catalog \| jq 'keys'` | 7 ключей, «Мой список» есть |
| Данные отдаются | `curl -s "…/catalog/list?plugin=watchlist&cat=films" \| jq .total_pages` | 1 |
| ТВ достаёт сервер | `ssh root@192.168.1.70 'curl -s -o /dev/null -w "%{http_code}" http://192.168.1.14:9118/catalog'` | 200 |
| ТВ реально ходит | `ssh root@192.168.1.70 'netstat -tn \| grep 9118'` | ESTABLISHED
ноль = Lampa не стартовала |
| Источник в списке | DevTools: `Object.keys(Lampa.Api.sources)` | **все** каталоги, вкл. «Мой список» |
Порядок разбора: сначала сеть с ТВ → потом `netstat` на ТВ → потом `/catalog` на сервере →
потом DevTools (состояние JS). **Если источник есть в `Api.sources` напрямую, но нет в
`Object.keys()` — это баг `enumerable` в `plugin.js`, а не проблема настроек.**
⚠️ `docker logs lampac` **HTTP-запросы не пишет** — по логам факт обращения ТВ не проверить.
## Внутреннее устройство Lampac (разведка 2026-09-22)
**33 модуля** в образе (`/lampac/module/`):
`AdminPanel`, `Adult`, **`Catalog`**, `Community`, **`DLNA`**, `DatabaseEditor`, `ExternalBind`,
`ForkPlayerXML`, `GStreamer`, `JacRed`, `Kit`, `LampaWeb`, `LampacApk`, `LogUserRequest-Lite`,
`MsxNative`, `Music`, `NextHUB`, **`Online`**, `OnlineAnime`, `OnlineENG`, `OnlineGEO`, `OnlinePaid`,
`OnlineRUS`, `OnlineUKR`, `PidTor`, `Potok`, `Proxy`, `SISI`, `Sync`, `Telemetry` (+ остальные).
**Конфиг — `current.conf`** внутри образа (`/lampac/current.conf`, 157 КБ, генерируется из `base.conf` +
`init.conf`). Рабочий каталог `/lampac`, файлы читаются **из корня**, не из `config/`.
Выключение модуля — двумя механизмами:
1. **`SkipModules`** в `init.conf` / `current.conf`
2. **`manifest.json`** в каталоге модуля (`"enable": true|false`)
⚠️ **`init.conf` НЕ в образе по умолчанию** — его читают из `/lampac/init.conf`, файл перекрывает
`base.conf`. Правка через `docker exec` **теряется при пересоздании контейнера**, поэтому
смонтирован с HDD (`./config-module/init.conf:/lampac/init.conf:ro`). См. раздел docker-compose выше.
⚠️ `current.conf` в образе (157 КБ) — **сгенерированный** результат (`guid`, `listen`, полный конфиг).
Это **не** входной файл, править его бессмысленно.
### Полезные роуты (проверены curl'ом)
| Роут | Код | Что |
|---|---|---|
| `/` | 200 | LampaWeb (UI, ``) |
| `/online.js` | 200 | плагин VOD — 70+ онлайн-источников |
| `/lg.ipk` | 200 | установщик клиента для LG (6.9 МБ) |
| `/sisi` | 200 | JSON-список платформ 18+ |
| `/dlna` | 200 | DLNA-модуль |
| `/ts/echo` | 200 | TorrServer (отдаёт версию, напр. `MatriX.135`) |
| `/catalog.js` | **200** | плагин каталогов (включён 2026-09-22) — см. раздел Catalog |
| `/catalog` | **200** | JSON каналов каталогов |
| `/watchlist.json`, `/watchlist-.json` | **200** | наш JSON-каталог watchlist |
| `/cub`, `/api`, `/lampac.js` | 404/500 | — |
⚠️ `wwwroot` отдаётся статикой: **любой** файл, положенный туда, доступен по HTTP
от корня (`http://:9118/<файл>`). Проверено записью пробного файла.
## Radarr/Sonarr как источник метаданных для каталога (2026-09-22)
**Зачем:** каталог watchlist нуждается в **TMDB ID и постерах** для элементов.
Список `movies-watchlist.md` — просто текст, там этого нет. Но фильмы добавлялись
в **Radarr**, который уже держит метаданные (тянет из TMDB при добавлении).
### Что отдаёт Radarr API
```bash
KEY=$(ssh kraken 'grep -oE "[^<]+" \
/srv/...6194539b.../docker/radarr/config/config.xml' | sed 's/<[^>]*>//g')
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq .
```
```json
{
"tmdbId": 1292695,
"title": "They Will Kill You",
"year": 2026,
"hasFile": true,
"monitored": true,
"images": [{"coverType": "poster",
"remoteUrl": "https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28R"}]
}
```
Маппинг в формат каталога: `tools.tmdbId` → `id`/`href`, `images[].remoteUrl` → `img`,
`title`+`year` → `title`, `hasFile` → признак «уже скачан».
### 🔴 Питфолл: `curl -sL`, без `-L` Radarr отдаёт 307
```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 # ✅ 50
```
### Окружение arr-стека на Кра́кене
| Сервис | Порт | Конфиг на HDD |
|---|---|---|
| radarr | 7878 | `/srv/...6194539b.../docker/radarr/config/` |
| sonarr | 8989 | аналогично |
| prowlarr | 9696 | аналогично |
| transmission | — | отдельный контейнер |
Постеры складываются в `/config/MediaCover/` (46 каталогов на 2026-09-22) — можно
раздавать их статикой через `wwwroot`, не дёргая CDN TMDB.
⚠️ `sqlite3` в контейнерах arr **нет** — БД (`radarr.db`) читать либо через `strings`,
либо вытащить файл на хост (там `/usr/bin/sqlite3` есть).
### Управление автоскачкой (два рычага, оба нужны)
| Рычаг | Команда | Эффект |
|---|---|---|
| `monitored` | `PUT /api/v3/movie/editor` | фильм не ищется/не качается, метаданные остаются |
| `rssSyncInterval` | `PUT /api/v3/config/indexer` | `0` = Radarr не ходит в RSS (сейчас 30 мин) |
⚠️ В Radarr **нет** состояния «источник, но не грузить» — это ровно `monitored: false`.
### Контур воспроизведения — не путать (✅ проверено 2026-09-22)
| Слой | Что делает | Пишет на HDD |
|---|---|---|
| Radarr + Sonarr + Transmission | автоскачка релизов в библиотеку | **да** |
| Lampac **TorrServer** (встроенный) | стриминг из UI Lampa по требованию | ❌ **нет — доказано** |
| Catalog «Мой список» | показ секций с постерами | ❌ ничего не качает |
Просмотр через Lampa **не пополняет библиотеку**: TorrServer отдаёт поток и выбрасывает.
`transmission` к Lampa отношения не имеет.
**Доказательство, что TorrServer не пишет на диск** — `/lampac/data/ts/settings.json`:
```bash
ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq .BitTorr
```
```json
{ "CacheSize": 100663296, // 96 МБ — RAM-кэш
"UseDisk": false, // 🔴 НА ДИСК НЕ ПИШЕТ
"TorrentsSavePath": "", // путь сохранения пуст
"TorrentDisconnectTimeout": 180, // раздача живёт 3 мин после остановки
"PreloadCache": 0, "ReaderReadAHead": 83 }
```
`UseDisk: false` — дефолт Lampac, настраивать не надо. Кэш Lampac целиком:
`/lampac/cache/` = 31 МБ (SQLite, Playwright, gstreamer-registry — **не фильмы**),
`/lampac/data/` = 44 МБ. Список торрентов пуст:
`curl -s -X POST .../ts/torrents -d '{"action":"list"}'` → `[]`.
Живость TorrServer: `curl -s "http://192.168.1.14:9118/ts/echo"` → `MatriX.135`.
### 🔴 Поиск торрентов — Jackett внутри Lampac (отдельный НЕ нужен)
**Ловушка:** в `lampainit.js` прописано `parser_torrent_type = 'jackett'`,
но **контейнера Jackett нет** в `docker ps`. Это не поломка — Lampac **сам отдаёт
Jackett-совместимый API**.
```bash
curl -s "http://192.168.1.14:9118/lampainit.js" | grep -oE "Lampa.Storage.set\('[^']+', ?'[^']*'\)"
# jackett_url = '192.168.1.14:9118' ← сам Lampac, не сторонний сервис
# jackett_key = '1'
# parser_torrent_type= 'jackett'
# parser_use = 'true'
# source = 'cub'
# torrents_use = true (lampa_settings)
```
⚠️ **API живёт на КОРНЕ, без префикса `/jackett`:**
```bash
curl -s "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix"
# ✅ 200 — реальные результаты: Tracker:rutor, MagnetUri, Size, Seeders, ffprobe
curl -s "http://192.168.1.14:9118/jackett/api/v2.0/indexers/all/results" # ❌ 404
```
**Вывод:** ничего поднимать не надо, поиск торрентов работает штатно.
### ⚠️ Sonarr — второй источник автоскачки (не только Radarr)
Alex упомянул его не зря. Замер 2026-09-22:
```bash
SKEY=$(ssh kraken 'grep -oE "[^<]+" \
/srv/...6194539b.../docker/sonarr/config/config.xml' | sed 's/<[^>]*>//g')
# → 15fbec32f1cb4b05b1c2b229ce27359b
curl -sL "http://localhost:8989/api/v3/series?apikey=$SKEY" | jq length # → 13 сериалов
curl -sL "http://localhost:8989/api/v3/series?apikey=$SKEY" \
| jq '[.[]|select(.monitored)]|length' # → 13 monitored
curl -sL "http://localhost:8989/api/v3/queue?apikey=$SKEY" | jq .totalRecords # → 19 в очереди
```
| Сервис | Объектов | monitored | Очередь |
|---|---|---|---|
| Radarr | 50 фильмов | **45** | **6** |
| Sonarr | 13 сериалов | **13** | **19** |
**Отключать надо оба** — иначе Sonarr продолжит качать сериалы.
### Transmission — замер
```bash
ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.json
# download-dir /downloads · rpc-port 9091 · speed-limit-down-enabled false
# /config/torrents/*.torrent = 257 файлов (накопленный список РАЗДАЧ, не активная качка)
# CPU 0.76% — простаивает. NetIO 290G↑/26.7G↓ (суммарно за жизнь контейнера)
```
Полный состав контейнеров Кра́кена:
`lampac · radarr · jellyfin · transmission · portainer · flaresolverr ·
prowlarr · sonarr · watchtower · rclone · xray-reverse-bridge · hermes-kraken · cloudflared`
## Что осталось
1. **Лимит кэша TorrServer** — сейчас дефолт (`CacheSize` 96 МБ, `UseDisk: false`).
Диск 396 ГБ свободно, на HDD TorrServer не пишет вовсе — вопрос неактуален.
2. **Шаг 7 плана — watchlist в UI Lampa** — ✅ **серверная часть сделана** 2026-09-22,
🔴 **архитектура признана неверной**, переделывается: см.
[[personal/plans/lampac-watchlist-plugin]], разделы «ПЕРЕДЕЛКА 2» и
«✅ ПРОВЕРКА "как это реализуемо"».
3. **Проверка полосы на торрент-источнике** (remux 60–80 Мбит) — не сделана.
4. **Постеры/карточка в каталоге** — не сделаны, `image` пустой. Решение — брать
из Radarr (`images[].remoteUrl`) или `MediaCover/`, см. раздел выше.
5. **Автоскачка Radarr + Sonarr** — 45 monitored / очередь 6 у Radarr,
13 monitored / очередь 19 у Sonarr. План отключения на апруве
([[personal/plans/lampac-watchlist-plugin]], «ПЕРЕДЕЛКА 2»).
## Карточка фильма в модуле Catalog — рабочая схема (проверено 2026-09-22)
Модуль `Catalog` (в образе `/lampac/module/Catalog/`). Каталог watchlist:
`sites/watchlist.yaml` + JSON в `wwwroot/`.
### Списки
```yaml
menu:
- categories:
Новинки: new # ПЕРВОЙ — она игнорирует format, берёт list.uri
Фильмы: films
Мультфильмы: cartoons
Аниме: anime
format:
"-": |
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
list:
uri: watchlist-new.json # ← сюда смотрит первая категория
content:
nodes: "$.items[*]"
href: { attribute: href } # href = TMDB URL → id = "movie/NNN"
```
### Карточка
`CardController.Index(plugin, uri, type)` — **`cat` не принимает**.
- `url = $"{init.host}/{uri}"` — дефолт, даёт `http://127.0.0.1:9118/movie/424781` → 500.
- `card_parse.initUrl` **исполняется как C#** (`CSharpEval`, объект
`CatalogInitUrlCard(host, args, uri, query, type)`) → можно собрать динамический URL.
- `card_parse.node` — **статичный JSONPath** (`json.SelectToken(node)`),
подстановок нет; `/` = вложенность.
Рабочая конфигурация:
```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-movie-424781.json`:
```json
{ "card": { "name": "...", "title": "...", "original": "...", "original_title": "...",
"year": "...", "rating": "...", "poster": "...", "img": "...",
"overview": "...", "genres": "...", "cast": "...", "runtime": 116 } }
```
Ключи дублируются (`name`+`title`, `poster`+`img`): `CardController` отдаёт
`title`/`original_title`, а `card_parse` ищет `name`/`original`.
### Эндпоинты
| Запрос | Назначение |
|---|---|
| `GET /catalog` | список каналов; `main` = категории |
| `GET /catalog/list?plugin=watchlist&cat=films&page=1` | элементы секции |
| `GET /catalog/card?plugin=watchlist&uri=movie/424781` | **карточка** |
🔴 **Не по `/card`** — там 404 и ложный вывод «карточка сломана».
### Файлы и права
- Статика раздаётся из `/lampac/wwwroot/` (`core` UseStaticFiles).
- Владделец — `1000:1000` (пользователь `lampac` в контейнере), каталоги `755`,
файлы `644`. Иначе `Permission denied` → **404**.
- macOS `tar` → AppleDouble `._*`, добавлять `--no-mac-metadata`.
### Деплой
```bash
python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
python3 deploy_lampac.py /tmp/wl-out --skip-plugin
```
Скрипты: `/Users/admin/lampac-tmp/`. Логика заливки — tar+scp+sudo,
`ssh -t` для sudo (без tty `sudo` требует пароль), каждая команда отдельным
вызовом (`&&`-цепочка рвёт tty).
### Связанные
## 🔴 ПОЧЕМУ КАРТОЧКА = «ЗДЕСЬ ПУСТО» (найдено 2026-09-22, вечер)
**Симптом:** данные в карточку приходят (title, img, overview), но в UI «здесь пусто»,
нет кнопки воспроизведения.
**Причина:** `plugin.js` при клике:
```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 считает карточку неполной → пустой экран.
### Два бага в цепочке, оба надо было починить
1. **`ModInit.cs:285` — `nodeValue` возвращает ЛЮБОЕ значение как строку.**
```csharp
string value = valueToken switch {
JValue jValue => jValue.Value?.ToString(), // ← число → "424781"
... };
```
Поэтому `tmdb_id` из JSON приходил строкой, и `if(json.tmdb_id)` давала не число.
2. **Кэш в памяти процесса переживает `docker restart`.**
`hybridCache` ключ `catalog:card:{plugin}:{uri}` живёт в памяти. При правке
данных старые ответы продолжают отдаваться. **Обязательно `docker restart lampac`
и проверять на НЕзакэшированном uri.**
### Фикс — `format` в `card_args`
`format` исполняется как C# (`CSharpEval`, объект `CatalogNodeValue(value, host)`)
и может вернуть настоящий тип, а не строку:
```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: 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 показывает в бейдже
```
### Рабочий ответ карточки (эталон)
```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`.**
### Про `CUB` — почему «только фильмы из списка»
CUB и «Мой список» — **два независимых источника Lampa**, одновременно показать
нельзя. Переключение: **Настройки → Остальное → Основной источник**.
Пункта «Источник» в настройках НЕ существует.
### Питфолл проверки на ТВ
Lampa на ТВ кэширует манифест и ответы. После правок **надо полностью
выгрузить приложение** (не свернуть), иначе клиент держит старый кэш.
---
## 🔴 Анатомия CUB-источника — что нужно, чтобы карточка открылась (разобрано 2026-09-22)
Разбор `app.min.js` (2.1 МБ; Лампа грузится с сервера, файл живёт в
`/lampac/wwwroot/lampa-main/app.min.js`). Копия локально:
```bash
ssh kraken "sudo docker cp lampac:/lampac/wwwroot/lampa-main/app.min.js /tmp/app-min.js"
scp -q kraken:/tmp/app-min.js /tmp/app-min.js
```
### Источники регистрируются как `sources = { tmdb: TMDB, cub: CUB }`
```js
var CUB = { main, menu, full: full$1, list, category, clear, person,
seasons, menuCategory, discovery, reactionsGet, reactionsAdd,
metadataGet, discussGet, extensions };
```
### `CUB.full()` (строка ~34240) — эталон карточки
```js
function full$1(params, oncomplite, onerror) {
var status = new Status(9);
if (Utils$1.dcma(params.method, params.id)) return onerror();
get$7('3/' + params.method + '/' + params.id
+ '?api_key=' + TMDB$1.key()
+ '&append_to_response=content_ratings,release_dates,external_ids,'
+ 'keywords,alternative_titles,images'
+ '&language=' + Storage.field('tmdb_lang'), params, function (json) {
if (json.status_code) return status.stop(), onerror();
json.source = 'cub'; // ← 🔴 CUB проставляет source САМ
...
status.append('movie', json);
}, ...);
}
```
**Вывод:** `json.source` — обязательное поле. CUB ставит его сам; наш плагин
делает то же (`plugin.js:42`: `json.source = params.source`).
### Как Explorer открывает карточку
`Explorer$1` (строка ~39403) читает `this.object.movie || this.object.card || {}`.
Данные приезжают из `Activity.push({ component: 'full', id, method, card, source })`.
Три места, где пушится `full`:
| Строка | Контекст | `source` |
|---|---|---|
| **44291** | кнопка «Смотреть» в фиде | `element.data.source \|\| 'tmdb'` |
| **40247** | кнопка «Назад» | `params.movie.source` |
| **18668** | уведомление | `element.card.source \|\| (ru/uk/be ? 'cub' : '')` |
🔴 **Фолбэк `|| 'tmdb'`** — если у элемента списка нет поля `source`, Lampa уйдёт
в **tmdb**, а не в наш каталог. Проверить, добавляет ли плагин `source` в элементы
`catalog/list` (в ответе `/catalog/list` этого поля **нет**).
### Что читает `full` из ответа карточки (`plugin.js:42-43`)
```js
json.source = params.source;
if (json.seasons && json.seasons.length) { ...tv/{tmdb_id}/season/{N}... }
if (json.tmdb_id) { ... cub.reactionsGet({id: json.tmdb_id,
method: json.original_name ? 'tv' : 'movie'}) ... }
if (json.credits) status.data['persons'] = json.credits;
if (json.recommendations) status.data['recomend'] = addSource(...);
if (json.similar) status.data['simular'] = addSource(...);
if (json.videos) status.data['videos'] = addSource(...);
status.append('movie', json);
```
→ **Обязательные поля:** `tmdb_id` (**число**), `original_name` или
`original_title` (определяет tv/movie), `img`, `overview`.
`seasons`/`credits`/`recommendations`/`similar`/`videos` — опциональны.
### ТВ-диагностика при «здесь пусто»
| Что | Как проверить |
|---|---|
| Есть ли соединения ТВ → Lampac | `ssh kraken "sudo ss -tn \| grep 9118"` |
| **Ноль соединений** | Lampa на ТВ **не общается с Lampac** — не запущена либо источник не активен |
| Захват запросов ТВ | `ssh kraken "sudo timeout 40 tcpdump -i any -n -A 'host 192.168.1.70 and port 9118'"` |
| `tcpdump` на Кра́кене | **не был установлен** → поставлен `sudo apt-get install -y tcpdump` (4.99.3) |
| Front app на ТВ | `script -q -c "luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}" /tmp/fg.txt` → вернул `com.lampac.tv` |
| `lampac_url` на ТВ | `loader.js`: `localStorage['lampac_url']`, дефолт `http://192.168.1.14:9118` ✅ |
| Кэш манифеста | `loader.js`: `cache_version = floor(Date.now()/9e5)` — смена сервера в течение ~15 мин не подхватится |
| Служба devtools | `com.webos.service.devtools` **НЕ существует** на webOS 5.4.2 Lite |
### ✅ РЕШЕНО: `partNext` — рабочий механизм (проверено 2026-09-22, сессия 8)
**Требование Alex:** секции Watchlist ВНУТРИ источника CUB, без ручного переключения.
Переключение источника — **не решение** (отвергнуто дважды).
#### Почему провалилась обёртка `cub.main` (3 итерации)
Родной `main$1()` в `app.min.js` объявлен **внутри замыкания Lampa** и зависит от
локальных `permit`, `get$7`, `Lang`, `permit.child_small`, `Helper$b`:
```js
function main$1() {
var parts_limit = 6;
var parts_data = [
function (call) { get$7('?sort=now_playing', params, function (json) {
json.title = Lang.translate('title_now_watch'); call(json); }, call, {life: day*2}); },
function (call) { if (permit.child_small) return call(); ... }
];
...
}
```
| Симптом | Замер CDP на ТВ | Корень |
|---|---|---|
| `cub.main({})` → вечный `pending`, 0 fetch | `{"pending":true}` через 15 с | `permit` = **undefined** вне замыкания → падение на первой проверке → callback не зовётся |
| секции не добавлялись, ошибок нет | `cubMainSrc` = `...var wl = Manifest.catalogs['watchlist']...` | `Manifest`/`Utils`/`network` = **undefined** вне замыкания → `ReferenceError` → `catch` → родной фид |
| «Нет подключения к сети» в логе ТВ | `{localhost}/catalog?uid=...&token={token}` | `Utils.account()` без `Manifest.api_host` подставляет `{localhost}` |
🔴 **Вывод: `cub.main` НЕВЫЗЫВАЕМ извне by design.** Обёртка — тупик, не «недоработка».
#### ✅ Правильный механизм: перехват `Lampa.Api.partNext`
Родной `main$1()` собирает `parts_data` — массив функций-секций — и отдаёт его в
**глобальную** `Lampa.Api.partNext(parts, parts_limit, partLoaded, partEmpty)`:
```js
function partNext(parts, parts_limit, partLoaded, partEmpty) {
var pieces = parts.filter(function (p) { return typeof p == 'function'; })
.slice(0, 0 + parts_limit);
if (pieces.length) {
new Progress().append(pieces).start(function (result) {
var data = result.filter(function (r) { return r && r.results && r.results.length; });
...
});
} else partEmpty();
}
```
Патч добавляет наши функции-секции в `parts` **до** вызова оригинала:
```js
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);
};
```
Секция — функция `call(json)`, те же контракты, что у родных CUB:
```js
function wlSection(name, url) {
return function (call) {
wlGet(url, function (json) {
json.title = name;
json.source = 'watchlist'; // ← обязательно, иначе карточка уйдёт в CUB/tmdb
if (Lampa.Arrays.isArray(json.results))
json.results.forEach(function (it) { it.source = 'watchlist'; });
call(json);
}, function () { call(); }); // ошибка → пустой вызов, пул не ломается
};
}
```
**Проверено на живом ТВ (CDP):** `pnCalls: 1`, `lastPartsCount: 28` — CUB собрал главную
из 28 функций (24 родные + наши 4). Activity `main`, `patched: true`, меню
`["Новинки","Фильмы","Мультфильмы","Аниме"]`. В DOM рендерились:
«Рассекая волны», «Хамнет», «Эхопраксия», «WeWork создание и крах компании».
#### 🔴 В замыкании доступны ТОЛЬКО глобальные объекты
| Нужно | ❌ Не работает в этом контексте | ✅ Работает |
|---|---|---|
| api_host | `Manifest.api_host` | `Lampa.Manifest.api_host` (фолбэк — хардкод `http://192.168.1.14:9118`) |
| URL-склейка | `Utils.account(url)` → `{localhost}` | **ручная:** `wlAbs(url)` = host + path |
| HTTP | `network.silent(...)` | `new Lampa.Reguest().silent(absURL, ok, fail)`, фолбэк `fetch` |
| список секций | `Manifest.catalogs['watchlist'].menu` | `window.__wl_menu` (заполняется в `loadCatalogs` из `/catalog`) |
```js
function wlAbs(url) {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
return wlHost() + (url.charAt(0) === '/' ? url : '/' + url);
}
```
🔴 **`Lampa.Reguest.silent(абсолютныйURL)` работает идеально** (замер: 33 фильма,
XHR уходит корректный). Проблема была только в относительных URL через `Utils.account`.
#### Сборщик `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 # обязательно
node -e "const s=require('fs').readFileSync('plugin-cub.js','utf8');let b=0;for(const c of s){if(c=='{')b++;if(c=='}')b--;}console.log('баланс:',b)" # → 0
```
Детерминирован: `[до startPlugin] + INJECT + [новый startPlugin] + '})();'`.
Инварианты: `WL-INJECT-START`=1, `function wlPatchPartNext`=1, `function startPlugin`=1,
`network.silent` в INJECT=0. Итог: **15476 Б** (в отдаваемой сборке **36956 Б**).
🔴 **Питфолл прошлого сборщика:** проверки `"X" not in s` шли **по уже модифицированной
строке** → повторный запуск вставлял INJECT дважды → лишняя `}` → `SyntaxError`,
баланс скобок `{`=123, `}`=122. Всегда `node --check` + баланс ПЕРЕД заливкой.
#### Деплой (полный порядок)
```bash
scp plugin-cub.js kraken:/srv/.../lampac/catalog/plugin.js
ssh kraken 'P=/srv/.../lampac/catalog; chmod 644 $P/plugin.js; chown 1000:1000 $P/plugin.js; rm -f $P/._*; docker restart lampac'
# проверка отдаваемой сборки (Staticache!):
curl -s http://192.168.1.14:9118/catalog.js -o /tmp/cj.js
grep -c 'WL-INJECT-START' /tmp/cj.js # → 1
grep -c 'wlPatchPartNext' /tmp/cj.js # → 3
```
⚠️ **ТВ держит CMY старый плагин в памяти.** После заливки обязателен перезапуск приложения
на ТВ. Признак незакрытого перезапуска: `String(Lampa.Api.sources.cub.main)` содержит
**старую** обёртку `wlEnsureMenu`, а `Lampa.Api.__wl_partnext` = true — **две версии
одновременно**, старая ломает главную (`uid`/`token`, вечный pending).
#### ⚠️ Перезапуск Lampa на ТВ — `luna-send` НЕ РАБОТАЕТ на этой прошивке
Проверено 2026-09-22: `luna-send` по ssh возвращает **пустоту** (не help, не ошибку),
и через файл (`sh /tmp/tv_restart.sh`), и с `reload:true`, и через `close`+`launch`.
Страница devtools не меняется (`reset=` в URL тот же) → приложение **не перезапускается**.
Рабочие альтернативы (не проверены до конца, следующая сессия):
- `Page.reload` + `Network.clearBrowserCache` через CDP (порт 9998)
- пульт: выйти из приложения и открыть заново
#### Питфолл: `eval(pluginCode)` внутри IIFE не создаёт глобальных функций
После `eval(code)` функции `wlHost()`/`wlAbs()` **снаружи не видны** (`ReferenceError`) —
они в замыкании IIFE. Это норма: `cub.main`/`partNext` их видят (одно замыкание),
внешние CDP-пробы — нет. Проверять только через `Lampa.Api.main()` либо чтение DOM.
#### Питфолл: `cub.list` без host → 404
`Lampa.Api.sources.cub.list({url:'/catalog/list?...'})` → **HTTP 404** (относительный URL).
Прямой `fetch` с абсолютным URL → **200**, 9496 байт. Ещё одно подтверждение того,
что в этом контексте нужны только абсолютные URL.
### 🔴 ТРЕБОВАНИЕ Alex: секции ВНУТРИ CUB, БЕЗ ручного переключения
**Дословно:** «надо блядь добавить в CUB список новые секции а не ручное блядь переключение!»
Текущий `plugin.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; // ← 🔴 молчит, если активен НЕ наш источник
...
e.abort();
}
});
```
**Почему не работает для CUB:** `Api.getCatalog()` читает
`Lampa.Manifest.catalogs[<текущий источник>]`. При выбранном источнике **CUB**
нашего каталога там нет → `return` → открывается родной раздел CUB.
**Что делать вместо этого (задача следующей сессии):**
дорисовывать секции в **корень CUB** — не перехватывать `menu`, а патчить рендер
главной (строки ~44260-44310: `feed` / `category_full`) либо добавлять ряды через
`Lampa.Listener.follow('activity')`. Цель: Alex открывает **обычный CUB** и **сверху
видит наши секции** с реальными элементами (постеры, рейтинги) — как в исходной
формулировке из [[personal/plans/lampac-watchlist-plugin]].
## 🔴🔴 ГЛАВНАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО»: кириллица в ключе каталога (2026-09-22)
**Это была настоящая причина. Не данные, не формат, не кэш — ключ каталога.**
### Как найдено
`tcpdump` на Кра́кене, фильтр по ТВ (`192.168.1.70`), клик по фильму в UI:
```
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
HTTP/1.1 400 Bad Request
```
`plugin=Мой список` — **URL-encoded кириллица**. Воспроизведено curl'ом:
```bash
curl "http://192.168.1.14:9118/catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=movie%2F424781&type=movie"
# → "init not found", HTTP 400
```
### Механика (две строки в `ModInit.cs` + одна в `ApiController.cs`)
`ApiController.cs:136` — ключ каталога в `/catalog`:
```csharp
string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site;
```
Без явного `plugin` ключ = **`displayname`** = «Мой список». ТВ берёт `plugin`
именно оттуда и шлёт его синхронно в `/catalog/list` и `/catalog/card`.
`ModInit.goInit` строки **51-52** — очистка имени перед поиском файла:
```csharp
site = site.ToLowerAndTrim();
site = Regex.Replace(site, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase);
```
🔴 **Регулярка вырезает ВСЮ кириллицу целиком.** `«Мой список»` → `""`.
Дальше: `File.Exists("/lampac/module/Catalog/sites/.yaml")` = false →
`FindSiteByDisplayName("")` = пусто (сравнивает с **уже очищенной** строкой) →
`return null` → `CardController`: `BadRequest("init not found")` → **HTTP 400**
→ в Lampa **«Здесь пусто»**.
**Ключевой вывод:** fallback `FindSiteByDisplayName` **никогда не сработает для
кириллического `displayname`** — regex убивает строку ДО сравнения. Это не лечится
названием файла, только явным `plugin` в латинице.
### ФИКС — явный `plugin` в YAML
```yaml
enable: true
jsonPath: true
displayindex: 1
displayname: Мой список # ← остаётся, это НАДПИСЬ в UI
plugin: watchlist # ← 🔴 ОБЯЗАТЕЛЬНО, латиница = ключ каталога
host: http://127.0.0.1:9118
```
`init.plugin` переопределяет ключ (`ApiController.cs:136`), и ТВ зовёт
`plugin=watchlist` → файл `sites/watchlist.yaml` находится штатно.
⚠️ **Побочный эффект (ожидаемый):** ключ в `/catalog` меняется `Мой список` →
`watchlist`. Надпись на ТВ не меняется, только внутренний идентификатор.
### Проверка
| Ключ | Запрос | Ответ |
|---|---|---|
| `Мой список` (было) | `plugin=%D0%9C%D0%BE%D0%B9...` | **400 `init not found`** |
| `watchlist` (стало) | `plugin=watchlist` | **200**, `tmdb_id: 424781` числом, `source: "movie"` |
### 🔴 ПРАВИЛО ДИАГНОСТИКИ (стоило часа)
**Проверять карточку curl'ом надо ТЕМ ЖЕ ключом, который шлёт ТВ.** Я подставлял
`plugin=watchlist` руками и получал 200, а ТВ слал `plugin=Мой список` и получал 400 —
и это выглядело как «сервер всё отдаёт, проблема в клиенте». Единственный надёжный
способ узнать реальный запрос — `tcpdump` на сервере во время клика:
```bash
ssh kraken "sudo timeout 40 tcpdump -i any -n -A 'host 192.168.1.70 and port 9118' 2>/dev/null | grep -aE 'GET /|HTTP/1'"
```
## Права, tar и macOS-мусор при заливке `cards/`
Три отдельных питфолла, каждый даёт **молчаливый 404** на статике:
1. **`macOS tar` кладёт AppleDouble-файлы `._*`** в архив → мусор в `wwwroot`.
Фикс: `tar --no-mac-metadata` + `sudo rm -f .../._*`.
2. **Права.** Если файлы приехали `root:root 600` — dotnet отдаёт **`Permission denied`**,
статика отвечает **404** (не 403!), и это выглядит как «файла нет».
Фикс: `chown -R 1000:1000` (пользователь `lampac` в контейнере) + `chmod -R 755`,
затем `chmod -R a+rX`.
3. **`find -exec` через `ssh -t` рвётся** — `find: missing argument to '-exec'`
(tty съедает `{} \;`). Фикс: два отдельных `chmod -R` вместо `find -exec`.
⚠️ Также: `&&`-цепочка в `ssh -t` рвёт tty-сессию («Connection closed»).
Каждую команду — отдельным вызовом.
## 🔴 Staticache — `/catalog.js` собирается и КЭШИРУЕТСЯ
`/catalog.js` — **не файл**, а **сборка**: Lampac берёт `plugin.js` из
`/lampac/module/Catalog/plugin.js`, подставляет `api_host` и `catalogs`
(отсюда ~18 КБ разницы от исходника) и кэширует результат через
атрибут `[Staticache]`.
**Питфолл, который стоил времени:** залил новый `plugin.js` (14099 Б,
патч на месте — `grep` подтверждает), а `/catalog.js` продолжал отдавать
**старую сборку** (32205 Б, патча нет). Выглядело как «патч не применяется».
```
=== патч в контейнере:
14099 /lampac/module/Catalog/plugin.js ← патч ЕСТЬ
=== что отдаёт клиенту:
32205 байт, injectWatchlistParts: 0 ← патча НЕТ
```
**Лечение:** `docker restart lampac` (сбрасывает Staticache-кэш), затем
проверять **размер и маркеры в отдаваемом файле**, а не только в файле на диске:
```bash
curl -s "http://192.168.1.14:9118/catalog.js" -o /tmp/cj.js
wc -c < /tmp/cj.js # размер сборки
grep -c injectWatchlistParts /tmp/cj.js # наш маркер
grep -c patchCubMain /tmp/cj.js
```
⚠️ Заголовки при этом отдают `Cache-Control: no-cache` — но это HTTP-кэш
клиента, а `Staticache` кэширует **на сервере**, поэтому заголовки не помогают
и клиентская очистка кэша проблему не решает.
## Внедрение секций Watchlist в CUB-фид (`plugin-cub.js`)
**Требование Alex:** секции Watchlist должны быть видны **ВНУТРИ источника CUB**,
без ручного переключения «Основной источник». Переключение — не решение.
### Почему штатный хук не подходит
`plugin.js` содержит `Lampa.Listener.follow('menu', ...)` + `e.abort()`:
он **перехватывает и ОТМЕНЯЕТ** разделы `movie/tv/cartoon/anime/relise`,
подменяя их нашими. Но `Api.getCatalog()` читает
`Manifest.catalogs[Lampa.Storage.field('source')]` — то есть каталог
**текущего** источника. При активном CUB нашего каталога там нет → хук молчит →
открывается родной CUB. Подмена работает только когда активен наш источник.
### Рабочая механика: обернуть `main` CUB-источника
`main$1()` в `app.min.js:33880` собирает массив `parts_data` — функций-загрузчиков,
каждая добавляет секцию на главную, и отдаёт через `Lampa.Api.partNext`.
Значит наши секции надо **добавить в этот массив**, а не подменять раздел.
Патч (`patch_cub_inject.py` → `plugin-cub.js`):
```js
function injectWatchlistParts(original) {
return function (params, oncomplite, onerror) {
original(params, function (cub_data) {
var wl = Manifest.catalogs['watchlist'];
if (!wl) return oncomplite(cub_data);
var menu = wl.menu || {}, keys = Object.keys(menu);
var list = Lampa.Arrays.isArray(cub_data) ? cub_data.slice() : [cub_data];
var pending = keys.length;
keys.forEach(function (name) {
network.silent(Utils.account(menu[name]), function (json) {
json.title = name; // заголовок секции
json.source = 'watchlist'; // источник для карточек
(json.results || []).forEach(function (it) { it.source = 'watchlist'; });
list.push(json); // ДОБАВЛЯЕМ, не заменяем
if (--pending === 0) oncomplite(list);
}, function () { if (--pending === 0) oncomplite(list); });
});
}, onerror);
};
}
function patchCubMain() {
var src = Lampa.Api.sources.cub;
if (!src || src.__wl_injected) return;
src.main = injectWatchlistParts(src.main);
src.__wl_injected = true;
}
```
Вызов — **по событию `app:ready`**, потому что нужны и `Lampa.Api.sources.cub`,
и загруженный `Manifest.catalogs['watchlist']`:
```js
if(window.appready){patchCubMain();}
else{Lampa.Listener.follow('app',function(e){if(e.type=='ready')patchCubMain();});}
```
🔴 **Ключевое в карточках наших секций — `source: 'watchlist'`.** Без него
клик уйдёт в CUB-карточку по TMDB-id и «Мой список» потеряется.
### Питфолл скрипта-патчера
🔴 **Порядок проверок в `patch_cub_inject.py` критичен.** Если сначала вставить
`INJECT` (который содержит текст `patchCubMain`), то последующая проверка
`"patchCubMain()" not in s` даст ложное «уже есть» и **вызов не вставится**.
Правильно: сначала проверить/вставить **ВЫЗОВ** (`if(window.appready){patchCubMain();}`),
и только потом — определение `INJECT`.
Проверка синтаксиса обязательна: `node --check plugin-cub.js`.
### Файлы патча
| Файл | Что |
|---|---|
| `plugin.js` | оригинал из образа (10670 Б) |
| `plugin-patched.js` | + `enumerable:true` (видимость источника в UI) |
| `plugin-cub.js` | + инжект секций в CUB-фид (**финальный, заливается**) |
| `patch_plugin.py` | делает `plugin-patched.js` |
| `patch_cub_inject.py` | делает `plugin-cub.js` из `plugin-patched.js` |
`deploy_lampac.py` шаг 4 берёт **`plugin-cub.js`**, с фолбэком на `plugin-patched.js`.
## Итоговое состояние (2026-09-22, конец сессии)
| Что | Статус |
|---|---|
| Ключ каталога | `plugin: watchlist` (латиница) ✅ |
| `/catalog` | `watchlist` первым, `main` = Новинки/Фильмы/Мультфильмы/Аниме |
| Карточки | **352 из 352** отдают числовой `tmdb_id` ✅ |
| Списки | films 360 (постеров 308), new 33 (30), cartoons 3 (3), anime 7 (7) |
| `tmdb_id` в карточке | числом через `format: "long.TryParse(value, out long v) ? v : 0"` |
| `runtime`, `vote_average` | тем же способом через `format` |
| `displayname` | «Мой список» — надпись в UI |
| **✅ Alex подтвердил в UI** | **фильмы Watchlist открываются и ПРОИГРЫВАЮТСЯ** |
| Инжект секций в CUB-фид | ✅ **механизм найден и проверен** (`partNext`), залито (36956 Б в сборке) |
| Причина «секции не появились» | 🔴 обёртка `cub.main` невызываема (`permit` в замыкании) → перешли на `partNext` |
| Осталось | перезапуск Lampa на ТВ (`luna-send` не работает — нужен пульт или CDP `Page.reload`) |
| Сборщик плагина | `build_plugin.py` (детерминированный, с инвариантами); `patch_cub_inject.py` **отменён** |
Скрипты: `/Users/admin/lampac-tmp/gen_watchlist_v2.py`, `deploy_lampac.py`
(`--skip-plugin` для быстрого прогона), `build_plugin.py`, `deploy_cub.py`,
`restart_lampa.sh`, `probe_tv.py`, `probe_cub_main.py`.
Проверка в шаге 7 валидирует именно ключ `watchlist` и предупреждает,
если в `/catalog` остался русский ключ.
## ⏳ Осталось добить (2026-09-22)
### 1. 5 фильмов с Кинопоиск-ID — «здесь пусто», без постера
`film/12432920/`, `film/5454868/`, `film/11888712/`, `film/11825624/`, `film/8184798/`
(«Рассекая волны: экспедиция „Лас Бальсас“», «Хамнет», «Цена вершины»,
«Астрал. Могильный ритуал 2», «О добре и зле»).
**Причина:** `href` в списке — ссылка на Кинопоиск, TMDB не нашёл (преимущественно
фильмы 2025), поэтому `id = film/NNNNN/` вместо `movie/NNNN`. Карточки
`card-film-...json` не существует → клик даёт пустоту; постера нет по той же причине.
**Решения (нужен выбор Alex):** заглушка с названием + кнопка поиска торрента,
либо дорисовывать `tmdb_id` вручную.
### 2. ~51 название без постера — проблема ЗАПИСИ В СПИСКЕ, не скрипта
Кэш резолвера `~/.lampac_tmdb_cache.json` содержит 51 запись со значением `null`.
По названиям видны три класса:
| Пример | В списке | TMDB знает как |
|---|---|---|
| `Плохие парни 1/2`, `Никто 1/2` | как есть | **ДВА фильма в одной строке** |
| `Солярис Тарковский`, `Идиот Бортко` | как есть | «Солярис» (1972) / «Идиот» (2003) |
| `Самолетом, поездом, машиной (1987)` | как есть | «Planes, Trains and Automobiles» |
Скрипт не может угадать, что `1/2` — два фильма, а «Тарковский» — уточнение
режиссёра. **Правится только список руками**, не генератором.
### 3. Секции CUB — проверить в UI
Инжект залит (`plugin-cub.js`, 35566 Б в отдаваемой сборке). Нужен перезапуск
Lampa на ТВ и проверка: секции Watchlist появляются **поверх родного фида CUB**.
### 🔴🔴 ШЕСТАЯ СЕССИЯ: ИСТИННАЯ ПРИЧИНА «СЕКЦИИ НЕ ПОЯВИЛИСЬ» — `Manifest` НЕДОСТУПЕН ВНУТРИ ОБЁРТКИ
Первая версия `injectWatchlistParts` читала `Manifest.catalogs['watchlist']` **напрямую**.
Проба через CDP на живом ТВ показала:
```
out.cubMainSrc = "function (params, oncomplite, onerror) { original(params, function (cub_data) {
try { var wl = Manifest.catalogs['watchlist']; if (!..."
out.err = "ReferenceError: Manifest is not defined"
wlInjected: true ← обёртка СТОЯЛА
вызов main({}) → len: 4, titles: ["Сейчас смотрят","Рекомендуем посмотреть","Последнее добавление","Огонь!"]
← только родные CUB, НИ ОДНОЙ нашей
```
**Корень:** `Manifest` объявлен **локально в замыкании плагина** (`/catalog.js`, IIFE).
Обёртка вызывается из другого контекста → `ReferenceError` → `catch` → молча
`oncomplite(cub_data)` с родными секциями. Ошибка **не видна в UI и не пишется в логи**.
**Фикс:** не полагаться на замыкание — сохранять `menu` в глобальный `window.__wl_menu`
при `loadCatalogs()`, в обёртке читать оттуда, с фолбэком на прямой запрос `/catalog`:
```js
// в loadCatalogs, ПОСЛЕ Manifest.catalogs=json;
if(json['watchlist']&&json['watchlist'].menu)window.__wl_menu=json['watchlist'].menu;
function wlEnsureMenu(cb) {
if (window.__wl_menu && Object.keys(window.__wl_menu).length) return cb(window.__wl_menu);
network.silent(Utils.account(Manifest.api_host + '/catalog'), function (json) {
var m = (json && json['watchlist'] && json['watchlist'].menu) || {};
window.__wl_menu = m; cb(m);
}, function () { cb(window.__wl_menu || {}); });
}
```
`wlEnsureMenu` вызывается внутри обёртки вместо прямого `Manifest.catalogs['watchlist']`.
⚠️ **`Manifest.api_host` в `wlEnsureMenu` — тоже замыкание.** Фолбэк сработает только
если `__wl_menu` уже заполнен `loadCatalogs`; в норме он заполнен. Если `__wl_menu`
пуст И `Manifest` недоступен — вернётся `{}` и родной фид.
### Сборка плагина: `build_plugin.py` вместо `patch_cub_inject.py`
🔴 **`patch_cub_inject.py` отменён — он не идемпотентен.** Разрезание по первому
`function startPlugin` + поиск `end` вставляло `INJECT` **дважды** и оставляло файл
с нарушенным балансом скобок (`{`=123, `}`=122 → `node --check` → `SyntaxError`).
**Новый сборщик — `build_plugin.py`, детерминированный:**
1. берёт базу `plugin-patched.js`;
2. вставляет `window.__wl_menu` в `loadCatalogs`;
3. вырезает ранее вставленный INJECT по маркеру `/* ==== WL-INJECT-START ==== */`;
4. разбивает файл на `[до startPlugin]` + `[startPlugin…]` + `[хвост })();]`;
5. собирает заново: `before` + `INJECT` + `NEW_START` + `tail`.
**Инварианты в конце скрипта** (падает с `[x] ИНВАРИАНТ НАРУШЕН`, если нарушено):
- `WL-INJECT-START` = **1**
- `function patchCubMain()` = **1**
- `function startPlugin` = **1**
⚠️ `patchCubMain();` встречается **2 раза** (в `window.appready` и в `app:ready`-листенере) —
это норма, в инвариант не входит (первая версия проверки ошибочно требовала 1).
Проверка: `node --check plugin-cub.js` + баланс скобок `{`/`}` = 0.
Итог: **13054 Б**, сборка на сервере **35566 → 35440 Б** (зависит от длины `catalogs`-JSON).
### Перезапуск Lampa на ТВ — правильный `luna-send`
🔴 **`luna-send` через `ssh 'script -q -c "luna-send … -a \"{...}\""'` НЕ парсит аргументы** —
печатает help. Кавычки в кавычках рвутся. **Самая надёжная форма — скрипт ФАЙЛОМ на ТВ:**
```bash
# 1. положить файл на ТВ
scp tv_restart.sh root@192.168.1.70:/tmp/
# 2. выполнить одной командой (без вложенных кавычек)
ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
```
```sh
#!/bin/sh
# /tmp/tv_restart.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"}'
```
⚠️ **Вариант с `'"'"'` (расшивка кавычек) НЕ сработал** — luna-send всё равно печатал
help-экран («`-a` send specified appId in message (default is none)»). Скрипт-файл —
единственный проверенный путь. Локальная копия: `/Users/admin/lampac-tmp/tv_restart.sh`.
⚠️ **`docker restart lampac` НЕ перезагружает плагин на ТВ.** После заливки `plugin.js`
ТВ продолжает держать **старую версию в памяти** — `cubMainSrc` через CDP показывает
старый код. Обязателен **перезапуск приложения на ТВ**.
### Диагностика рантайма Lampa через CDP (websocket-client)
⚠️ Модуля `ws` в Node нет, `websockets` в Python нет → использовать **`websocket-client`**
(установлен). Скрипты в `/Users/admin/lampac-tmp/`: `probe_tv.py`, `probe_globals.py`,
`probe_cache.py`, `probe_cub_main.py`, `probe_inject.py`, `probe_wait.py`, `probe_calls.py`,
**`probe_ui.py`** (финальная приёмка — читает заголовки секций из DOM).
```bash
WS=$(curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl' | head -1)
python3 probe_ui.py "$WS"
```
🔴 **`probe_cub_main.py` (вызов `cub.main`) НЕ годится как приёмка** — этот метод
невызываем извне (см. «СЕДЬМАЯ СЕССИЯ» в разделе механики) и всегда возвращает
`pending`/4 родные секции, **даже когда секции реально работают в UI**.
**Приёмка только через `probe_ui.py`** — переход `Lampa.Router.call('main')` + чтение
`document.querySelectorAll('.items-line__title')`.
Полезные поля пробы `probe_tv.py`: `Lampa.Storage.field('source')`,
`Object.keys(Lampa.Api.sources)`, `Lampa.Api.sources.cub.__wl_injected`,
`typeof Lampa.Api.sources.cub.main`.
⚠️ `Manifest` / `Utils` / `network` внутри `Runtime.evaluate` **не видны**
(`ReferenceError`) — они в замыкании плагина. Не использовать в пробах.
⚠️ **`eval(pluginCode)` внутри IIFE не создаёт глобальных функций** — `wlHost()`,
`wlAbs()` снаружи дают `ReferenceError`. Это нормально: `cub.main` их видит
(одно замыкание), внешние пробы — нет.
## Питфоллы проверки на ТВ (сводка)
1. **Lampa кэширует манифест и ответы.** После правок — **полностью выгрузить
приложение**, не сворачивать. `loader.js` использует `cache_version`
(9e5 мс ≈ 15 мин), плюс свой `localStorage`.
2. `luna-send` пишет в **tty**, не в stdout → обёртка
`script -q -c "luna-send ..." /tmp/o.txt`, читать файл.
(Доки: `family/how-to/tv-luna-send.md`.)
3. Реальный запрос клиента ловить `tcpdump` на сервере — см. «ПРАВИЛО ДИАГНОСТИКИ» выше.