From 6746ac09907b9b187e2996a6a2c420e67482af32 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 22 Sep 2026 16:37:53 +0600 Subject: [PATCH] [2026-09-22] eagle: personal/plans/lampac-watchlist-plugin.md personal/tech/lampac-deployment.md --- personal/plans/lampac-watchlist-plugin.md | 195 +++++++++++++++++++++- personal/tech/lampac-deployment.md | 168 ++++++++++++++++++- 2 files changed, 355 insertions(+), 8 deletions(-) diff --git a/personal/plans/lampac-watchlist-plugin.md b/personal/plans/lampac-watchlist-plugin.md index a10ba82f..3df81b61 100644 --- a/personal/plans/lampac-watchlist-plugin.md +++ b/personal/plans/lampac-watchlist-plugin.md @@ -7,7 +7,7 @@ related: - '[[family/projects/watchlist-sync]]' - '[[family/projects/watchlist-automation]]' - '[[family/documents/movies-watchlist]]' -status: cub-sections-patched-awaiting-tv-restart +status: cub-sections-working-awaiting-tv-restart-deploy tags: - lampac - lampa @@ -90,16 +90,18 @@ updated: '2026-09-22' > Плюс устранён питфолл `Staticache` (кэш собранного `/catalog.js` переживал > заливку и выглядел как «патч не применяется» → `docker restart lampac`). > -> **Осталось (2026-09-22, конец шестой сессии):** -> 1. **Секции CUB в UI** — 🔴 инжект залит, но требовал **перезапуска Lampa на ТВ** -> (причина «ничего не поменялось» — `Manifest` в обёртке, см. выше). После перезапуска -> проверить через `probe_cub_main.py`: должно быть **8 секций** (4 родные CUB + 4 наши). -> 2. **5 фильмов с Кинопоиск-ID** (`film/NNNNN/`) — «Рассекая волны: экспедиция „Лас Бальсас“», +> **Осталось (2026-09-22, конец СЕДЬМОЙ сессии):** +> 0. 🔴 **ЗАЛИТЬ `plugin-cub.js` НА КРА́КЕН + ПЕРЕЗАПУСТИТЬ LAMPA НА ТВ.** Механизм +> (`partNext`) **проверен на живом ТВ через CDP** — секции рендерятся в DOM главной CUB +> (подробности + список probe-скриптов — раздел «СЕДЬМАЯ СЕССИЯ»). Команда деплоя ждёт +> апрува: `scp plugin-cub.js kraken:.../catalog/plugin.js` → `docker restart lampac` → +> `sh /tmp/tv_restart.sh` → `python3 probe_ui.py`. +> 1. **5 фильмов с Кинопоиск-ID** (`film/NNNNN/`) — «Рассекая волны: экспедиция „Лас Бальсас“», > «Хамнет», «Эхопраксия», «Цена вершины», «Астрал. Могильный ритуал 2», «О добре и зле», > WeWork (2024). > TMDB не нашёл (2025 год) → нет карточки и постера → клик даёт «здесь пусто». > **Нужен выбор Alex:** заглушка с кнопкой поиска торрента либо дорисовать `tmdb_id`. -> 3. **~51 название без постера** — проблема ЗАПИСИ В СПИСКЕ, не скрипта: +> 2. **~51 название без постера** — проблема ЗАПИСИ В СПИСКЕ, не скрипта: > `Плохие парни 1/2` (ДВА фильма одной строкой), `Солярис Тарковский` (уточнение > режиссёра), `Самолетом, поездом, машиной (1987)` (русский аналог). > Генератор такое угадать не может — **правится только список руками**. @@ -220,6 +222,185 @@ updated: '2026-09-22' > **Не тронуто, ждёт решения Alex:** 310 фильмов из списка, которых нет в Radarr > (51 без постера, идут через фолбэк Кинопоиска). Без апрува не делать. +--- + +## 🔴🔴 СЕДЬМАЯ СЕССИЯ: РАБОЧИЙ МЕХАНИЗМ НАЙДЕН — ПЕРЕХВАТ `Lampa.Api.partNext` + +**Статус: ✅ секции Watchlist рендерятся ВНУТРИ главной CUB. Проверено на живом ТВ через CDP.** + +Alex: «В cub секции сейчас смотрят, рекомендуем, последнее добавленное, огонь, в высоком +качестве. Все.» и «до сих пор хамнет, рассекая волны, эхопраксия, WeWork итд — без постера +и "здесь пусто"» + «Ты вообще нихуя не поменял!!». + +### Итог проверки на ТВ (CDP, `probe_ui.py`) + +``` +ЛЕВО (1) → главная CUB, activity: "main" +titles: ["Простите за беспокойство", "Смерть единорога", + "Рассекая волны: экспедиция «Лас Бальсас»", "Я ругаюсь", "F1", + "Мастерская Сатаны", "Хамнет: История, вдохновившая «Гамлета»", ..., + "Эхопраксия", "WeWork создание и крах компании", ...] +``` + +То есть **наши фильмы физически в DOM главной CUB** — те самые, что Alex называл. + +### Три бага подряд — почему «ничего не поменялось» + +**❌ Баг 1 (главный): обёртка `Api.sources.cub.main` не работает В ПРИНЦИПЕ.** + +Родной `main$1()` (в `app.min.js`, @1257163) внутри замыкания Lampa зависит от +**локальных** `permit`, `get$7`, `Lang`: + +```js +function main$1() { + var parts_limit = 6; + var parts_data = [ + function (call) { get$7('?sort=now_playing', params, ...); }, // ← get$7 локальная + function (call) { if (permit.child_small) return call(); ... } // ← permit локальная + ]; + ... +} +``` + +При вызове извне замыкания (`cub.main({})`) он падает на `permit.child_small` +(`permit` → **undefined** в глобальном контексте) и **никогда не зовёт callback** → +вечный `pending`. Проверено CDP-пробой: `{"pending":true}` через 15 секунд, ни одного fetch. +**Обёртка мертва by design** — не потому что код плохой, а потому что метод невызываемый извне. + +**❌ Баг 2: `Manifest` / `Utils` / `network` недоступны вне замыкания плагина.** + +Замер через CDP (`probe_globals.py`): +```json +{"Manifest_global":"undefined","Lampa":"object", + "Utils":"undefined","network":"undefined"} +``` +Все три — **локальные переменные плагина**. Первая версия инжекта читала +`Manifest.catalogs['watchlist']` напрямую → `ReferenceError` → `catch` → молча +отдавался родной фид. **Ошибки нет ни в UI, ни в логах.** + +**❌ Баг 3: `Utils.account()` без `Manifest.api_host` подставляет `{localhost}`.** + +```js +Lampa.Utils.account(menu[name]) // menu[name] = "/catalog/list?plugin=watchlist&cat=new" +``` +→ `{localhost}/catalog?uid=...&token={token}` → в логе ТВ: +``` +Request GET time: 4ms error of {localhost}/catalog?uid=...&token={token} + : Нет подключения к сети +``` +`Utils.account` **требует** `Manifest.api_host` в области видимости. Вне замыкания его нет. + +### ✅ РЕШЕНИЕ: перехват `Lampa.Api.partNext` + +`partNext` — **глобальный** API (`Lampa.Api.partNext`), которым сам CUB собирает главную: + +```js +function partNext(parts, parts_limit, partLoaded, partEmpty) { + var pieces = parts.filter(function (p) { return typeof p == 'function'; }) + .slice(0, 0 + parts_limit); + ...Progress().append(pieces).start(...) +} +``` + +CUB передаёт туда `parts_data` — **массив функций-секций**. Перехватив `partNext`, +наши секции добавляются **в тот же пул**, что и родные: работают на любой странице, +независимо от источника, без `permit` и без `params`. + +```js +function wlPatchPartNext() { + var orig = Lampa.Api.partNext; + Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) { + if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) { + var menu = window.__wl_menu; + if (menu && Object.keys(menu).length) { + parts.__wl_done = true; // идемпотентность + Object.keys(menu).forEach(function (name) { + parts.push(wlSection(name, menu[name])); + }); + } + } + return orig.apply(this, arguments); + }; +} +``` + +Плюс обязательные спутники (глобальные только!): +- `wlHost()` — `Lampa.Manifest.api_host`, фолбэк хардкод `192.168.1.14:9118` +- `wlAbs(url)` — склейка host + относительный путь **вручную**, `Utils.account` НЕ использовать +- `wlGet(url, ok, fail)` — `new Lampa.Reguest().silent(absoluteURL, ...)`, фолбэк `fetch` +- `window.__wl_menu` — заполняется в `loadCatalogs` при загрузке `/catalog`; фолбэк — сетевой запрос + +### Сборщик `build_plugin.py` (детерминированный, заменяет `patch_cub_inject.py`) + +```bash +cd /Users/admin/lampac-tmp +python3 build_plugin.py # plugin-patched.js + INJECT → plugin-cub.js +node --check plugin-cub.js # обязательная проверка +``` + +Инварианты на выходе (скрипт сам их проверяет и падает при нарушении): +`INJECT=1`, `def wlPatchPartNext=1`, `startPlugin=1`, `network.silent в INJECT=0`. + +🔴 **Почему не `patch_cub_inject.py`:** он был неидемпотентен — при повторном запуске +вставлял INJECT **дважды** и ломал баланс скобок (`SyntaxError: Unexpected token '}'`), +потому что проверки шли по уже модифицированной строке. `build_plugin.py` режет файл +детерминированно: `[до startPlugin] + INJECT + [новый startPlugin] + '})();'`. + +### 🔴 CDP-пробы на живой ТВ (новый инструментарий) + +ТВ LG отдаёт **Chrome DevTools Protocol** на порту **9998**: +```bash +curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl' +# ws://192.168.1.70:9998/devtools/page/ +``` + +Клиент на Mac — `websocket-client` (модуль `websockets` НЕ установлен): +```python +import websocket +ws = websocket.create_connection(WS_URL, timeout=25) +ws.send(json.dumps({"id":1,"method":"Runtime.evaluate", + "params":{"expression":EXPR,"returnByValue":True,"awaitPromise":True}})) +``` + +Скрипты в `/Users/admin/lampac-tmp/`: + +| Скрипт | Что проверяет | +|---|---| +| `probe_tv.py` | базовое состояние: `source`, `hasCub`, `cubKeys`, `wlInjected`, `sourcesList` | +| `probe_globals.py` | доступность `Manifest`/`Utils`/`network` в глобальном контексте | +| `probe_cache.py` | откуда ТВ берёт `catalog.js` (script-теги, кэш) | +| `probe_cub_main.py` | что отдаёт обёрнутый `cub.main` (сколько секций) | +| `probe_inject.py` | инжект нового `plugin-cub.js` в рантайм БЕЗ деплоя (быстрая итерация) | +| `probe_wait.py` | ждёт завершения вызова + перехватывает `console.log` | +| `probe_ui.py` | **финальная приёмка:** переход на главную + чтение заголовков из DOM | +| `restart_lampa.sh` / `tv_restart.sh` | перезапуск Lampa на ТВ через `luna-send` | + +🔴 **Питфолл `luna-send` через ssh:** аргументы JSON не проходят через +`ssh 'script -q -c "luna-send ... -a {\"id\":...}"'` — luna-send печатает help +(«send specified appId in message (default is none)»). **Фикс:** положить скрипт на ТВ +файлом (`scp tv_restart.sh`) и вызвать `sh /tmp/tv_restart.sh`: + +```sh +#!/bin/sh +luna-send -n 1 -f luna://com.webos.applicationManager/close '{"id":"com.lampac.tv"}' +sleep 5 +luna-send -n 1 -f luna://com.webos.applicationManager/launch '{"id":"com.lampac.tv"}' +``` + +🔴 **`eval()` не делает функции глобальными.** Инжект `eval(code)` внутри IIFE плагина +оставляет `wlHost`/`wlAbs` в замыкании → снаружи (`wlHost()`) `ReferenceError`. +Это нормально: `cub.main` их видит, внешние пробы — нет. Проверять только через +вызов `Lampa.Api.main()` или чтение DOM. + +### Перезапуск Lampa на ТВ — обязателен + +🔴 **`docker restart lampac` НЕ перезагружает плагин в памяти ТВ.** `catalog.js` +закэширован браузером webOS. Признак «ТВ видит старую версию»: +`String(Lampa.Api.sources.cub.main)` содержит старый код. + +Порядок: залить `plugin.js` на Кра́кен → `docker restart lampac` (сброс `Staticache`) → +**перезапустить Lampa на ТВ** (`tv_restart.sh`) → `probe_ui.py`. + ## 🔴 КАРТОЧКА: механика card_parse (разобрано по коду 2026-09-22) Клик по элементу каталога ведёт в `/catalog/card?plugin=watchlist&uri=&type=movie`. diff --git a/personal/tech/lampac-deployment.md b/personal/tech/lampac-deployment.md index bfa4b13a..18fd978d 100644 --- a/personal/tech/lampac-deployment.md +++ b/personal/tech/lampac-deployment.md @@ -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`)