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

126 KiB
Raw Blame History

title, created, updated, type, namespace, tags, related
title created updated type namespace tags related
Lampac — развёртывание и эксплуатация (Кра́кен) 2026-09-22 2026-09-22 tech personal
lampac
torrserver
docker
kraken
streaming
webos
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 на ТВ).

📦 Репозиторий скриптов: git_admin/lampac-scripts (2026-09-22)

~/lampac-tmp/ — НЕ git-репо (свалка одноразовых пробников, 50+ файлов). Прод-генератор жил там без версионирования — ошибка, исправлено.

Репозиторий: /Users/admin/Developer/lampac-scripts/ (gitea, private) Remote (ЧИСТЫЙ URL, токен не в URL): https://git.mallexxx.duckdns.org/git_admin/lampac-scripts.git Токен — в ~/.git-credentials (600). Питфолл №1 из family/how-to/gitea-config.md.

/Users/admin/Developer/lampac-scripts/
  ├── scripts/gen_watchlist.py      (из gen_watchlist_v2.py — ПРОД-генератор)
  ├── scripts/deploy_lampac.py
  ├── scripts/mark_missing.py       (в прод НЕ ставить — см. план, Шаг 1.3)
  └── tests/accept_step1.py, tests/accept_step1_order.py

Создание репо через API — ~/lampac-tmp/create_gitea_repo.py (Python, а не bash: подстановка $( … | sed … ) вырезается фильтром безопасности — питфолл №2 из family/how-to/gitea-config.md).

🔄 Ночной крон на Кра́кене — что делает автоматика

~/scripts/watchlist-nightly.sh, cron 01:00:

sync-vault (pull) → process-thumbs --apply → resolve --recheck --apply
                  → sync-down --apply --batch 2 → sync-up --apply → sync-vault (push)

~/scripts/watchlist-catalog.sh, cron **30 2 * * *** — генерация JSON + рестарт lampac. ~/scripts/watchlist-cleanup.sh, cron **0 2 * * *** — сносит просмотренное при заполнении диска.

🔴 resolve --recheck --apply снимает маркер сам, когда фильм находится. Это работа резолвера watchlist_sync/resolver.py — НЕ генератора каталога и НЕ ручная. resolve пропускает позиции с ❓/🗓️ (флаг ambiguous в WatchItem, parser.py).

🔴 Права на файлы в 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)

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:

{
  "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:

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 отбрасывает сайт:

if (init == null || !init.enable || init.menu == null || init.hide) continue;

В логах — тишина. Сайт молча исчезает из /catalog. Обе формы (список и словарь) ломают.

main Catalog генерирует сам из menu — в JSON-ответе /catalog присутствуют оба ключа с одинаковым содержимым. Писать main в YAML не нужно и нельзя.

⚠️ menu обязателен — он регистрирует канал, а не подменяет разделы.

🔴 Питфолл №4: элемент без href выбрасывается

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:

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:

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

// глобальная функция 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 до вызова оригинала:

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:

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)
function wlAbs(url) {
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    return wlHost() + (url.charAt(0) === '/' ? url : '/' + url);
}

Сборщик build_plugin.py

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) — основной инструмент отладки

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

# ❌ печатает 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'
#!/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 «здесь пусто».

Сигнатура роута (критично!):

[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):

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 НЕ пишутся!):

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)

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.

Управление

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 шагов с бэкапами и самопроверкой. Запуск:

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 без ключа = сеть есть).

Проверка работоспособности

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 из Кра́кена:

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 падает

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), и первая же команда отваливается. Каждую команду вызывать отдельно:

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 не виден внутри.

Проверка:

ssh kraken "sudo docker inspect lampac --format '{{json .Mounts}}'" | jq -r '.[].Destination'

Фикс:

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, откат после провала)

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 ломают одинаково — и список, и словарь:

main:                       # вариант 1 — список: НЕ работает
  - categories:
      Фильмы: films
main:                       # вариант 2 — словарь: НЕ работает
  Фильмы: http://...

⚠️ Ошибка не логируется. В логах Lampac — тишина (docker logs lampac | grep -i yaml пусто). Сайт просто не появляется в /catalog. Диагностика только перебором YAML.

Кто на самом деле подменяет родные разделы — plugin.js, а не YAML:

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:

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()), два вхождения:

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:

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 их не содержит.

# 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 каталогов).

{
  "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 — ИСТОЧНИК АВТОСКАЧКИ (в коде!)

# 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 мин — пусто.

Правильные эндпоинты (проверено):

# отмена загрузки — файлы на диске НЕ трогаются
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, ...остальное}

Откат:

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-хендлер:

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 мин после остановки
# конфиг 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 прописывает:

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 сам:

# ⚠️ на КОРНЕ, БЕЗ префикса /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:

# 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:

"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.

Порядок установки:

# 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, и прочитать временный файл:

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
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(), два вхождения):

// БЫЛО (баг)
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:

# docker-compose.yml
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
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 этого не дают.

# цель отладки + 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:

pages  = json.load(urllib.request.urlopen("http://192.168.1.70:9998/json", timeout=8))
target = next(p for p in pages if "com.lampac.tv" in p.get("url",""))
ws = websocket.create_connection(target["webSocketDebuggerUrl"], timeout=10)
# Runtime.evaluate: Lampa.Storage.field('source')
#                   Object.keys(Lampa.Api.sources)
#                   Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список')

Готовые скрипты: /tmp/lampa_probe.py (состояние источника), /tmp/lampa_probe3.py (асинхронный вызов menu() каталога).

Ловушки:

⚠️ Замер соединений — на ТВ, не на сервере. sudo ss -tn | grep :9118 на Кра́кене сразу после старта Lampa даёт 0 (ложный вывод «не ходит»). Через ~30 с: ssh root@192.168.1.70 'netstat -tn | grep 9118' → 1 ESTABLISHED + 6 TIME_WAIT.

⚠️ launch через luna требует экранированного JSON в двойных кавычках, иначе errorCode: 2, invalid parameters. getForegroundAppInfo {} работает без кавычек.

⚠️ tcpdump на Кра́кене нет — не ставили, DevTools дал больше.

⚠️ luna://com.webos.service.capture/takeScreenShot не существует на этом ТВ (Unknown method) — скриншот так не снять.

Диагностика «каталога нет в UI»

Проверка Команда Норма
Плагин отдаётся curl -sI http://192.168.1.14:9118/catalog.js 200, ~32 КБ
Каталоги зарегистрированы curl -s http://192.168.1.14:9118/catalog | jq 'keys' 7 ключей, «Мой список» есть
Данные отдаются curl -s "…/catalog/list?plugin=watchlist&cat=films" | jq .total_pages 1
ТВ достаёт сервер ssh root@192.168.1.70 'curl -s -o /dev/null -w "%{http_code}" http://192.168.1.14:9118/catalog' 200
ТВ реально ходит ssh root@192.168.1.70 'netstat -tn | grep 9118' ESTABLISHED
ноль = Lampa не стартовала
Источник в списке DevTools: Object.keys(Lampa.Api.sources) все каталоги, вкл. «Мой список»

Порядок разбора: сначала сеть с ТВ → потом netstat на ТВ → потом /catalog на сервере → потом DevTools (состояние JS). Если источник есть в Api.sources напрямую, но нет в Object.keys() — это баг enumerable в plugin.js, а не проблема настроек.

⚠️ docker logs lampac HTTP-запросы не пишет — по логам факт обращения ТВ не проверить.

Внутреннее устройство Lampac (разведка 2026-09-22)

33 модуля в образе (/lampac/module/): AdminPanel, Adult, Catalog, Community, DLNA, DatabaseEditor, ExternalBind, ForkPlayerXML, GStreamer, JacRed, Kit, LampaWeb, LampacApk, LogUserRequest-Lite, MsxNative, Music, NextHUB, Online, OnlineAnime, OnlineENG, OnlineGEO, OnlinePaid, OnlineRUS, OnlineUKR, PidTor, Potok, Proxy, SISI, Sync, Telemetry (+ остальные).

Конфиг — current.conf внутри образа (/lampac/current.conf, 157 КБ, генерируется из base.conf + init.conf). Рабочий каталог /lampac, файлы читаются из корня, не из config/.

Выключение модуля — двумя механизмами:

  1. SkipModules в init.conf / current.conf
  2. manifest.json в каталоге модуля ("enable": true|false)

⚠️ init.conf НЕ в образе по умолчанию — его читают из /lampac/init.conf, файл перекрывает base.conf. Правка через docker exec теряется при пересоздании контейнера, поэтому смонтирован с HDD (./config-module/init.conf:/lampac/init.conf:ro). См. раздел docker-compose выше.

⚠️ current.conf в образе (157 КБ) — сгенерированный результат (guid, listen, полный конфиг). Это не входной файл, править его бессмысленно.

Полезные роуты (проверены curl'ом)

Роут Код Что
/ 200 LampaWeb (UI, <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

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 .
{
  "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

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:

ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq .BitTorr
{ "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.

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:

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:

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 — замер

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/.

Списки

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)), подстановок нет; / = вложенность.

Рабочая конфигурация:

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:

{ "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.

Деплой

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 при клике:

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 возвращает ЛЮБОЕ значение как строку.

    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)) и может вернуть настоящий тип, а не строку:

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 показывает в бейдже

Рабочий ответ карточки (эталон)

{ "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). Копия локально:

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 }

var CUB = { main, menu, full: full$1, list, category, clear, person,
            seasons, menuCategory, discovery, reactionsGet, reactionsAdd,
            metadataGet, discussGet, extensions };

CUB.full() (строка ~34240) — эталон карточки

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)

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:

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):

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 до вызова оригинала:

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:

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)
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)

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 + баланс ПЕРЕД заливкой.

Деплой (полный порядок)

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 подменяет разделы через:

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'ом:

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:

string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site;

Без явного plugin ключ = displayname = «Мой список». ТВ берёт plugin именно оттуда и шлёт его синхронно в /catalog/list и /catalog/card.

ModInit.goInit строки 51-52 — очистка имени перед поиском файла:

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

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 на сервере во время клика:

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-кэш), затем проверять размер и маркеры в отдаваемом файле, а не только в файле на диске:

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):

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']:

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:

// в 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. Кавычки в кавычках рвутся. Самая надёжная форма — скрипт ФАЙЛОМ на ТВ:

# 1. положить файл на ТВ
scp tv_restart.sh root@192.168.1.70:/tmp/
# 2. выполнить одной командой (без вложенных кавычек)
ssh root@192.168.1.70 'sh /tmp/tv_restart.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).

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 на сервере — см. «ПРАВИЛО ДИАГНОСТИКИ» выше.