[2026-09-22] eagle: personal/plans/lampac-watchlist-plugin.md personal/tech/lampac-deployment.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-22 16:37:53 +06:00
1 parent 8441f3a4cb
commit 6746ac0990
2 files changed
+355 -8

No files matched your search

+167 -1
View File
@@ -216,6 +216,168 @@ id = href,
`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}"`
@@ -279,12 +441,16 @@ curl -s -o /tmp/c.json -w "HTTP %{http_code}\n" \
Лежит **в образе** (`/lampac/module/Catalog/plugin.js`), не на HDD. Что делает:
- `main()` — итерирует `catalog.main`, собирает секции **СВЕРХУ** родного каталога
- `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`)