Files
obsidian-vault/personal/tech/lampac-deployment.md
T

2297 lines
123 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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=<p>&cat=<c>&page=N` | список элементов категории |
| `/catalog/card?plugin=<p>&uri=<id>&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<ActionResult> Index(string plugin, string uri, string type)
```
🔴 **Параметра `cat` НЕТ.** Карточка знает только `uri` и `type`. Поэтому
`initUrl: $"…/cards-{cat}.json"` даёт `cards-.json` → 404 → HTTP 500.
**Что можно и что нельзя подставлять:**
| Поле | Механика | Подстановка |
|---|---|---|
| `initUrl` | `CSharpEval.Execute<string>` с `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-<media>-<id>.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 <watchlist.md> <out_dir>`
Читает `.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-<code>.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 "<ApiKey>[^<]+</ApiKey>" \
/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/<id>/`,
в 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/<id>?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/<site>.yaml` — `host`, `menu`, `search`, `list`, `content` |
| Переопределения | `override/<site>.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 (!<arch>), НЕ 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 <br>ноль = 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, `<base href="/lampa-main/">`) |
| `/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-<code>.json` | **200** | наш JSON-каталог watchlist |
| `/cub`, `/api`, `/lampac.js` | 404/500 | — |
⚠️ `wwwroot` отдаётся статикой: **любой** файл, положенный туда, доступен по HTTP
от корня (`http://<host>:9118/<файл>`). Проверено записью пробного файла.
## Radarr/Sonarr как источник метаданных для каталога (2026-09-22)
**Зачем:** каталог watchlist нуждается в **TMDB ID и постерах** для элементов.
Список `movies-watchlist.md` — просто текст, там этого нет. Но фильмы добавлялись
в **Radarr**, который уже держит метаданные (тянет из TMDB при добавлении).
### Что отдаёт Radarr API
```bash
KEY=$(ssh kraken 'grep -oE "<ApiKey>[^<]+</ApiKey>" \
/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 "<ApiKey>[^<]+</ApiKey>" \
/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` на сервере — см. «ПРАВИЛО ДИАГНОСТИКИ» выше.