3288 lines
210 KiB
Markdown
Executable File
3288 lines
210 KiB
Markdown
Executable File
---
|
||
created: '2026-09-22'
|
||
namespace: personal
|
||
related:
|
||
- '[[personal/plans/lampac-kraken]]'
|
||
- '[[personal/tech/lampac-deployment]]'
|
||
- '[[family/projects/watchlist-sync]]'
|
||
- '[[family/projects/watchlist-automation]]'
|
||
- '[[family/documents/movies-watchlist]]'
|
||
- '[[personal/plans/lampac-watched-to-watchlist]]'
|
||
status: fifth-fix-2026-09-25-global-inject-flag-deployed-awaiting-verdict
|
||
tags:
|
||
- lampac
|
||
- lampa
|
||
- watchlist
|
||
- catalog
|
||
- webos
|
||
- plugin
|
||
- kraken
|
||
- radarr
|
||
- sonarr
|
||
- torrserver
|
||
- tmdb
|
||
- card-parse
|
||
- feed
|
||
- matcher-parity
|
||
- partnext
|
||
- latency
|
||
- wlonmain
|
||
- wlprefetch
|
||
- activity-hook
|
||
title: Watchlist в UI Lampa — каталог из списка
|
||
type: plan
|
||
updated: '2026-09-25'
|
||
---
|
||
|
||
# Watchlist в UI Lampa — каталог из списка
|
||
|
||
## 🔍 2026-09-25: РАССЛЕДОВАНИЕ — фид виден на всех экранах + «долго грузится»
|
||
|
||
## 🟡 2026-09-25 (продолжение): ДУБЛИ СЕКЦИЙ — «watchlist снова уплыл вниз»
|
||
|
||
**Симптом Alex:** «Ну чето нифига не поменялось. Главная - фильмы - главная -
|
||
watchlist снова уплыл вниз».
|
||
|
||
**Диагноз по DOM-замеру — не «уплыл», а ДУБЛЬ.** В `document.querySelectorAll('.items-line')`
|
||
оказалось **13 заголовков вместо 9**:
|
||
|
||
```
|
||
0:Новинки 1:Фильмы 2:Мультфильмы 3:Аниме ← наши (вверху, верно)
|
||
4:Сейчас смотрят ... 8:В высоком качестве ← родные CUB
|
||
9:Новинки 10:Фильмы 11:Мультфильмы 12:Аниме ← НАШИ ВТОРОЙ РАЗ (внизу)
|
||
```
|
||
|
||
Визуально нижний комплект читается как «секции уплыли вниз», но это **второй экземпляр**:
|
||
первый остался наверху, Lampa его не удаляет.
|
||
|
||
### 🔴 КОРНЕВАЯ ПРИЧИНА: флаг вставки велся на МАССИВЕ, а не глобально
|
||
|
||
Было: `if (... && !parts.__wl_done) { parts.__wl_done = true; wlInject(...) }`.
|
||
|
||
🔴 **`parts` приходит НОВЫМ массивом на каждый вызов `partNext`** — на нём нет пометки
|
||
от прошлого вызова, поэтому `!parts.__wl_done` **истинно всегда**, и `wlInject`
|
||
добавлял второй комплект через `unshift`. Флаг, повешенный на объект пула, не работает.
|
||
|
||
Плюс `window.__wl_done = false` в хуке сбрасывал **не ту переменную**: проба показала
|
||
`wl_done = None` (undefined в глобальной области) — хук не влиял ни на что.
|
||
|
||
### ✅ ФИКС: глобальный `window.__wl_injected`
|
||
|
||
```js
|
||
// wlPatchPartNext:
|
||
if (wlOnMain() && Lampa.Arrays.isArray(parts) && !window.__wl_injected) {
|
||
window.__wl_injected = true; // ГЛОБАЛЬНО, не на parts
|
||
wlInject(parts, menu);
|
||
}
|
||
// wlWatchActivity: при уходе с главной разрешаем вставку при возврате
|
||
if (comp !== 'main') { window.__wl_injected = false; return; }
|
||
```
|
||
|
||
Логика: ушли с главной → флаг снят; вернулись → вставили ОДИН раз; повторные вызовы
|
||
`partNext` на той же главной → флаг держит, дублей нет.
|
||
|
||
**Инвариантов в сборщике — 20.** Добавлены: `глобальный флаг вставки` (=2, синхронная
|
||
и async-ветки), `флаг ставится` (=2), `флаг снимается при уходе` (=1),
|
||
`НЕТ старого parts.__wl_done` (=0).
|
||
|
||
🔴 **Проверка, что правка реально на ТВ:** проба `window.__wl_activity_hook === true`
|
||
(уникальный маркер этой сборки). `ready`/`patch` для этого НЕ годятся — они истинны
|
||
и для старой версии, из-за чего три круга правок выглядели «не применёнными».
|
||
|
||
## 🔍 2026-09-25: РАССЛЕДОВАНИЕ — фид виден на всех экранах + «долго грузится»
|
||
|
||
**Жалоба Alex (дословно):** «есть проблема с watchlist фидом в лампе: он отображается
|
||
не только на "главной" но даже когда я выбираю категорию например "документальные"
|
||
в разделе Фильмы, то есть независимо от активного экрана. и он как будто долго
|
||
прогружается относительно основного CUB списка, почему он не моментально проявляется,
|
||
он же статичный».
|
||
|
||
**Статус: ✅ ПРИЁМКА ПРОЙДЕНА 2026-09-25 — фильтр работает. Фид снят со всех экранов
|
||
кроме главной. Осталось: предзагрузка `__wl_menu` (латентность) + 66 с на карточке.
|
||
См. «✅ ИТОГ 2026-09-25» в конце раздела.**
|
||
|
||
### ✅ РЕШЕНИЕ: `wlOnMain()` — белый список по активному activity
|
||
|
||
Alex: «В смысле всё кроме? **Только главный экран!**» — предикат сделан строгим
|
||
(`=== 'main'`), не «всё кроме списка исключений». Любое незнакомое имя activity
|
||
обязано по умолчанию секции НЕ получать.
|
||
|
||
⚠️ **ПЕРВАЯ ВЕРСИЯ (ниже) НЕРАБОЧАЯ — оставлена как история ошибки.** Она читала
|
||
`a.activity.url`; правильный уровень — `a.url` (измерено на ТВ, см. «РЕГРЕСС И ВТОРОЙ
|
||
ФИКС»). Актуальный текст — `wlUrl()` + `wlOnMain()` в разделе регресса.
|
||
|
||
```js
|
||
// ❌ НЕРАБОЧАЯ ПЕРВАЯ ВЕРСИЯ — оставлена как история ошибки, НЕ КОПИРОВАТЬ
|
||
function wlOnMain() {
|
||
try {
|
||
var a = (window.Lampa && Lampa.Activity && Lampa.Activity.active)
|
||
? Lampa.Activity.active() : null;
|
||
var act = (a && a.activity && a.activity.url) ? a.activity.url : '';
|
||
if (act === 'main') return true;
|
||
console.log('[watchlist] не главная (' + (act || '?') + ') — секции пропущены');
|
||
return false;
|
||
} catch (e) {
|
||
console.log('[watchlist] wlOnMain error', e);
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
Гард в перехватчике (`plugin-cub.js:193`):
|
||
```js
|
||
if (wlOnMain() && Lampa.Arrays.isArray(parts) && !parts.__wl_done) { ... wlInject(parts, menu); }
|
||
```
|
||
|
||
🔴 **Второе, неочевидное изменение:** `parts.__wl_done = true` теперь ставится ТОЛЬКО
|
||
внутри ветки фактической вставки. Раньше флаг мог выставиться до готовности
|
||
`window.__wl_menu` — и тогда вставка была бы закрыта НАВСЕГДА, главная осталась бы без
|
||
секций после возврата из категории.
|
||
|
||
**Файлы:**
|
||
- `/Users/admin/lampac-tmp/plugin-cub.js` — рабочая сборка (бэкап
|
||
`plugin-cub.js.bak-before-filter-<ts>`)
|
||
- `/Users/admin/lampac-tmp/build_plugin.py` — тот же текст в константе `INJECT`
|
||
(сборщик детерминированный; `plugin-cub.js` руками не правим) + **2 новых инварианта**:
|
||
`wlOnMain def` и `wlOnMain guard` — сборка падает, если фильтр пропал
|
||
|
||
**Проверки перед заливкой (все пройдены):**
|
||
```
|
||
python3 build_plugin.py → 10/10 инвариантов, 17241 байт
|
||
node --check plugin-cub.js → SYNTAX OK
|
||
```
|
||
|
||
**Деплой (выполнен):**
|
||
1. бэкап боевого — `catalog/plugin.js.bak-before-mainfilter-20260925-220809` (17476 Б)
|
||
2. `scp plugin-cub.js kraken:...6194539b.../lampac/catalog/plugin.js` → 19576 Б,
|
||
md5 `53ccdc306cf6a14f343eaeb67a8a29c3`
|
||
3. bind-mount проверен: `docker exec lampac grep -c wlOnMain
|
||
/lampac/module/Catalog/plugin.js` → **3**
|
||
4. по HTTP тоже отдаётся: `curl http://127.0.0.1:9118/catalog.js` → 41050 Б, `wlOnMain` ×3
|
||
|
||
**Осталось — перезапуск Lampa на ТВ** (webOS кэширует `catalog.js`, `docker restart
|
||
lampac` его НЕ сбрасывает):
|
||
```bash
|
||
scp /Users/admin/lampac-tmp/tv_restart.sh root@192.168.1.70:/tmp/ \
|
||
&& ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
|
||
```
|
||
|
||
**Приёмка = видно в UI** (не «API отвечает»): главная — 4 секции Watchlist сверху;
|
||
Фильмы → документальные — секций НЕТ. Если фид вылезет ещё где-то — в логе ТВ будет
|
||
строка `[watchlist] не главная (<имя>)`, по ней добавить имя в разбор.
|
||
|
||
**Что НЕ трогалось:** `window.__wl_menu`, `wlGet`/`wlAbs`/`wlHost`, `unshift`,
|
||
`watchlist.yaml`, `sync_down.py`, nightly-пайплайн.
|
||
|
||
### Причина 1 (диагноз) — в `plugin-cub.js` НЕ БЫЛО фильтра по экрану
|
||
|
||
Файл: `/Users/admin/lampac-tmp/plugin-cub.js` (сборка `build_plugin.py`;
|
||
это тот же файл, что залит в Lampac как `module/Catalog/plugin.js`).
|
||
|
||
`wlPatchPartNext()` — перехват глобального `Lampa.Api.partNext` — до фикса выглядел так:
|
||
|
||
```js
|
||
Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
|
||
try {
|
||
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
|
||
var menu = window.__wl_menu;
|
||
if (menu && Object.keys(menu).length) {
|
||
parts.__wl_done = true;
|
||
wlInject(parts, menu); // ← БЕЗУСЛОВНАЯ вставка
|
||
}
|
||
```
|
||
|
||
**Ни одного обращения к активному экрану** — это не регресс, а исходное отсутствие
|
||
проверки. Механика: `partNext` — глобальный API, через него строят страницу ВСЕ
|
||
источники. `plugin.js:30` (`main()`) и `plugin.js:36` (`category()`) **оба** вызывают
|
||
`Lampa.Api.partNext(parts_data, parts_limit, ...)`. Отсюда:
|
||
- CUB рисует главную → перехват срабатывает → наши 4 секции сверху;
|
||
- вход в категорию («документальные» внутри Фильмов) → Lampa перестраивает страницу
|
||
через **тот же** `partNext` → перехват срабатывает снова → фид снова сверху.
|
||
|
||
`wlInject()` кладёт секции через `unshift` в **начало** пула, поэтому они рисуются
|
||
поверх содержимого любой категории — ровно то, на что жаловался Alex.
|
||
|
||
### Причина 2 — «долго грузится»: замеры (не догадки)
|
||
|
||
Всё замерено `curl` на Кра́кене по `127.0.0.1:9118` (порт только изнутри хоста):
|
||
|
||
| Запрос | Время | `size_download` |
|
||
|---|---|---|
|
||
| `GET /catalog/list?plugin=watchlist&cat=films` — повторный | **4 мс** | 107 640 |
|
||
| тот же — **первый (холодный кэш)** | **2 250 мс** | 107 640 |
|
||
| `GET /watchlist-films.json` — чистая статика мимо Catalog | **1 мс** | 88 156 |
|
||
| `GET /catalog/list?plugin=watchlist&cat=new` | 2–147 мс | 8 512 |
|
||
| **`GET /catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie`** | **66 118 мс** | 939 |
|
||
|
||
🔴 **ПИТФОЛЛ ЗАМЕРА: `curl -w '%{size_download}'` показывает размер СЖАТОГО тела.**
|
||
Я сначала прочитал 107 640 как «фид всего 105 КБ, грузиться нечему» — это ошибка.
|
||
Реальные размеры: `watchlist-films.json` = **88 156 несжатых** при 347 позициях,
|
||
`cards.json` = **548 219** (данные `ls -la` на Кра́кене). Для замеров объёма брать
|
||
`ls -la` или `%{size_download}` вместе с `Accept-Encoding: identity`.
|
||
|
||
**Что из этого следует:**
|
||
|
||
1. **4 мс на прогретом кэше** — данные отдаются мгновенно, «статичный» — правда.
|
||
2. **2.25 с на ПЕРВОМ холодном запросе категории** — это парсинг JSON парсерами
|
||
модуля Catalog (`content.nodes`, `args`) при пустом `hybridCache`. Дальше — кэш.
|
||
Отсюда ощущение «не моментально проявляется»: категория, которую давно не открывали,
|
||
платит 2+ секунды, а родной CUB-список тем временем уже отрисован.
|
||
3. **66 секунд на карточке фильма** — отдельный, более серьёзный баг. 66 с на файл
|
||
939 байт из локального каталога — это не диск. `card_parse.initUrl` исполняется как
|
||
C#, `node: card`, читается `wwwroot/cards/card-movie-NNN.json`. Внутри
|
||
`CardController` что-то ждёт (таймаут внешнего вызова / `reactionsGet` к cub).
|
||
**Не диагностировано до конца — следующий шаг.**
|
||
4. **Наши секции стартуют позже родных:** `partNext` собирает пул, `parts_limit = 6`,
|
||
наши секции идут через `unshift` и грузятся отдельным HTTP-запросом на каждую
|
||
(`/catalog/list?plugin=watchlist&cat=<code>`) — 4 запроса. Родной фид CUB приходит
|
||
одним запросом, поэтому «проявляется» раньше.
|
||
|
||
### Контракт Lampac (проверено, НЕ трогать без причины)
|
||
|
||
`catalog/sites/watchlist.yaml` на Кра́кене
|
||
(`/srv/dev-disk-by-uuid-6194539b-.../lampac/catalog/sites/watchlist.yaml`):
|
||
|
||
- `plugin: watchlist` (🔴 без него ключ = `displayname` = «Мой список» → `ModInit.goInit`
|
||
вырезает кириллицу регуляркой `[^a-z0-9\-]` → site="" → HTTP 400 → «ЗДЕСЬ ПУСТО»).
|
||
- `host: http://127.0.0.1:9118`, категории `Новинки/Films/Мультфильмы/Аниме`.
|
||
- 🔴 **Первая категория в списке ИГНОРИРУЕТ `format`** и берёт `list.uri` напрямую —
|
||
поэтому первой стоит «Новинки» (`list.uri: watchlist-new.json`).
|
||
- `card_parse.initUrl` конвертирует `uri` `movie/424781` → `cards/card-movie-424781.json`,
|
||
`node: card`. `card_args.tmdb_id` обязан быть **числом** (через `format: long.TryParse`),
|
||
иначе карточка = «здесь пусто».
|
||
|
||
**Nightly-пайплайн** (`/home/kraken/scripts/watchlist-nightly.sh`, крон `0 1 * * *`,
|
||
пользователь `kraken` НА КРА́КЕНЕ): `sync-vault pull` → `process-thumbs --apply` →
|
||
`resolve --recheck --apply` → **`sync-down --apply`** (сборка фида, `LAMPAC_ROOT=`UUID-путь)
|
||
→ `sync-up --apply` → backup `TimeCode.sql` (`sqlite3 .backup`) → `sync-from-lampac --apply`
|
||
→ `sync-vault push`. Рестарта lampac в скрипте НЕТ и он не нужен (`wwwroot/` bind-mount,
|
||
тот же inode) — но при залипшем каталоге в UI помогает обновление страницы / переключение
|
||
категории. На 2026-09-25 `watchlist-catalog.sh` уже убран в `.bak-removed-20260924-181942`,
|
||
отдельной строки крона `30 2 * * *` в crontab **нет** (п.2 из списка незакрытого — закрыт).
|
||
|
||
### 🔴 РЕГРЕСС И ВТОРОЙ ФИКС 2026-09-25 (после первой заливки)
|
||
|
||
**Alex после перезапуска Lampa:** «Нету теперь списка на главной. Смотри логи.»
|
||
Секции исчезли ПОЛНОСТЬЮ — первая версия фильтра оказалась нерабочей.
|
||
|
||
**Логи сняты с живой Lampa через CDP** (ТВ `192.168.1.70:9998`, скрипт
|
||
`/Users/admin/lampac-tmp/wl_console.py` + `/tmp/wl_probe*.py`; `websocket-client`,
|
||
`Runtime.enable` + `Log.enable`):
|
||
|
||
```
|
||
act = undefined ← фильтр сравнивал с 'main', получал пустое
|
||
comp = [object Object]
|
||
wl_menu_type = undefined ← window.__wl_menu НЕ создан
|
||
catalog_all = NOMANIFEST ← Lampa.Manifest.catalogs вообще пуст
|
||
patch = true ← перехватчик стоял, фильтр работал
|
||
src = cub
|
||
```
|
||
|
||
**🔴 КОРНЕВАЯ ПРИЧИНА №1 — `url` лежит НА ВЕРХНЕМ уровне activity, а не во вложенном.**
|
||
Измерено на живом ТВ (`/tmp/wl_probe5.py`, вывод дословно):
|
||
|
||
```
|
||
Lampa.Activity.active() → {url, title, component, source, page, params, activity}
|
||
Lampa.Activity.active().activity → {component, object, slide, body, is_stopped, is_started}
|
||
↑ url ЗДЕСЬ НЕТ
|
||
```
|
||
|
||
Первая версия читала `active().activity.url` → **всегда `undefined`** → `=== 'main'`
|
||
всегда `false` → секции пропускались на главной ВСЕГДА. Фильтр не «слишком строгий» —
|
||
он смотрел не туда. **Проверять форму объекта ЗАМЕРОМ, а не по памяти: в доках
|
||
сессии 7 форма не была зафиксирована.**
|
||
|
||
**🔴 КОРНЕВАЯ ПРИЧИНА №2 — `Manifest.catalogs` пуст** → `loadCatalogs()` не получил
|
||
`/catalog` от Lampac → `window.__wl_menu` не создан → **вставлять нечего**, даже если
|
||
фильтр пропускает. Строгая первая версия заодно отсекала ЕДИНСТВЕННЫЙ вызов `wlMenu()`,
|
||
который дозагружает меню повторным запросом. То есть фильтр не только не пропускал —
|
||
он снял путь восстановления.
|
||
|
||
**Фикс (залит 2026-09-25 22:14):** новая функция `wlUrl(a)` читает **оба уровня**,
|
||
верхний первым, вложенный — фолбэк; плюс смягчающая ветка «пустой `url` + пустой
|
||
`catalogs` = инициализация главной → пропустить»:
|
||
|
||
```js
|
||
function wlUrl(a) {
|
||
if (!a) return '';
|
||
if (a.url) return a.url; // ← ПРАВИЛЬНЫЙ уровень
|
||
if (a.activity && a.activity.url) return a.activity.url; // фолбэк
|
||
return '';
|
||
}
|
||
|
||
function wlOnMain() {
|
||
var a = (window.Lampa && Lampa.Activity && Lampa.Activity.active)
|
||
? Lampa.Activity.active() : null;
|
||
var act = wlUrl(a);
|
||
if (act === 'main') return true;
|
||
if (!act) { // главная ещё инициализируется
|
||
var c = (window.Lampa && Lampa.Manifest && Lampa.Manifest.catalogs) || {};
|
||
if (!Object.keys(c).length) return true;
|
||
}
|
||
console.log('[watchlist] не главная (' + (act || '?') + ') — секции пропущены');
|
||
return false;
|
||
}
|
||
```
|
||
|
||
Ложная вставка на чужом экране при этом невозможна: без `__wl_menu` вставлять нечего.
|
||
|
||
**Сборка и деплой (все инварианты пройдены):**
|
||
```
|
||
python3 build_plugin.py → 12/12, 18281 байт
|
||
node --check plugin-cub.js → SYNTAX OK
|
||
```
|
||
Новые инварианты сборщика: `wlUrl def`, `wlUrl верхний уровень`
|
||
(`if (a.url) return a.url;`) — сборка падает, если чтение уровня откатится назад.
|
||
Бэкап: `catalog/plugin.js.bak-before-fixurl-<ts>`. Залито → 20934 Б,
|
||
`docker exec lampac grep -n "function wlUrl" -A4 /lampac/module/Catalog/plugin.js` → на месте.
|
||
|
||
**⚠️ ПЕРВЫЙ ЗАМЕР ПОСЛЕ ЗАЛИВКИ — приёмка на нём НЕ пройдена** (оставлено как
|
||
история): `url = ""` на ОБОИХ уровнях, `hasCats = false`, `menu = 0`. Alex на это
|
||
ответил: «Ты заебал. Ты блядь задачу решаешь или мозг мне ебешь?! Я жду от тебя:
|
||
готово, проверяй.» — **правильный ответ на такой сигнал: доделать и доложить,
|
||
не задавать вопросов.** Ниже выяснилось, что пустой `url` был замером в момент
|
||
загрузки, а не поломкой; приёмка пройдена после полной инициализации Lampa.
|
||
|
||
**Следующий шаг был:** проверить боевой запрос Lampa → Lampac `/catalog`. ✅ Выполнено —
|
||
`/catalog` жив с обеих сторон: с Кра́кена `http=200 size=21470 t=0.44s` (холодный),
|
||
по LAN с Mac `http=200 size=21470 t=0.01s`, тело содержит корректный
|
||
`watchlist.menu` = `{Новинки, Фильмы, Мультфильмы, Аниме}`. **Бэкенд был исправен —
|
||
проблема была только в гонке замера.**
|
||
|
||
### ✅ ИТОГ 2026-09-25: ПРИЁМКА ПРОЙДЕНА
|
||
|
||
**Alex:** «Так. Есть. Но сильно после первых специй.» — секции на главной **появились**,
|
||
фильтр работает. Осталась претензия к задержке появления.
|
||
|
||
#### Доказательство работы фильтра (замер на живой Lampa после перезапуска)
|
||
|
||
```
|
||
ready = true
|
||
patch = true ← перехватчик установлен
|
||
act_url = "movie" ← в момент замера экран = Фильмы (не главная)
|
||
menu = Новинки,Фильмы,Мультфильмы,Аниме ← __wl_menu заполнен, всё есть
|
||
dom_titles = Сейчас смотрят | Клиника | Мыс страха | Мейр из Исттауна | ...
|
||
↑ Watchlist-секций в DOM НЕТ
|
||
```
|
||
|
||
🔴 **Ключевой вывод:** на экране `movie` (Фильмы) наших секций в DOM **нет**, при том что
|
||
`menu` заполнен и `patch` активен. Это и есть доказательство, что фильтр режет по экрану,
|
||
а не «случайно ничего не нашёл»: вставлять было чем, но `wlOnMain()` вернул `false`.
|
||
|
||
#### 🔴 Почему «сильно после первых специй» — АРХИТЕКТУРНАЯ, не сетевая
|
||
|
||
Разобрано по коду `loadCatalogs()` + `wlPatchPartNext`:
|
||
|
||
1. `loadCatalogs()` идёт по ветке `else` и делает `network.silent('/catalog', ...)` —
|
||
**только после ответа** пишет `Manifest.catalogs` и `window.__wl_menu`.
|
||
То есть меню приходит **асинхронно**.
|
||
2. Главная CUB рисуется **своим** `cub.main()`, **не дожидаясь** этого запроса. Родные
|
||
секции («Сейчас смотрят», «Рекомендуем посмотреть») стартуют сразу.
|
||
3. Первый вызов `partNext` застаёт `__wl_menu` пустым → уходит в ветку `wlMenu()` —
|
||
**асинхронный** запрос `/catalog`. Пока он летит, родные секции уже отрисованы.
|
||
4. Когда `/catalog` вернулся, `wlInject` делает `unshift`, но `partNext` уже отработал
|
||
срез `slice(0, parts_limit)` (`parts_limit = 6`). Наши секции попадают в пул только
|
||
на **следующем** раунде, который добирает `data.length >= 3`.
|
||
|
||
**Отсюда отставание:** наши 4 секции — это отдельный сетевой запрос на каждую
|
||
(`/catalog/list?plugin=watchlist&cat=<code>` = 4 запроса), стартующие позже родных,
|
||
плюс попадание в пул не первым раундом. Родной фид CUB приходит одним запросом.
|
||
|
||
**План ускорения (согласован Alex: «Коммить и переходим к предзагрузке»):**
|
||
подставить `menu` из `/catalog` **синхронно**, ДО первого `partNext` — в `startPlugin()`,
|
||
параллельно с `loadCatalogs()`. Тогда к моменту отрисовки главной `__wl_menu` уже в
|
||
памяти, `wlInject` срабатывает в первом же раунде, секции встают в голову без отставания.
|
||
|
||
#### ✅ РЕШЕНО: репозиторий `~/Developer/lampac-scripts` — история НЕ в `lampac-tmp`
|
||
|
||
**Изначальная ошибка вывода (сохранена как урок).** Проверка «в лоб» дала:
|
||
|
||
```
|
||
/Users/admin/lampac-tmp → fatal: not a git repository
|
||
/srv/.../lampac (на Кра́кене) → fatal: not a git repository
|
||
```
|
||
|
||
Отсюда был сделан **неверный вывод** «коммитить некуда». На деле репозиторий
|
||
**существовал давно** — я искал его не там, потому что в доке пути к плагину были
|
||
записаны как `/Users/admin/lampac-tmp/`.
|
||
|
||
🔴 **Настоящее место: `~/Developer/lampac-scripts`** (Gitea
|
||
`git_admin/lampac-scripts`), там же `scripts/`, `tests/`. Alex дважды указал на это
|
||
(«Проект на маке в developer это в доке записано. Ведь так?!», «И на gitea залит»).
|
||
**Урок: прежде чем объявить «некуда коммитить» — искать репозиторий по
|
||
`~/Developer/`, а не только рядом с файлом.**
|
||
|
||
**Репы на Кра́кене:** только `docker/`, `obsidian/`, `watchlist-sync/`. В `lampac/`
|
||
git нет и не заводится — бэкапы там `.bak-*` (закрыты `.gitignore`).
|
||
|
||
| Артефакт | Путь |
|
||
|---|---|
|
||
| **Исходник (источник истины)** | `~/Developer/lampac-scripts/scripts/build_plugin.py` |
|
||
| **Собранный выход** | `~/Developer/lampac-scripts/scripts/plugin-cub.js` |
|
||
| Скрэтч, вне git | `/Users/admin/lampac-tmp/` |
|
||
| Боевой на Кра́кене | `.../lampac/catalog/plugin.js` + `.bak-*` рядом |
|
||
|
||
`build_plugin.py` держит весь `INJECT` в себе → файл воспроизводится
|
||
**детерминированно**, с 20 проверками инвариантов.
|
||
|
||
#### ✅ ЗАКОММИЧЕНО 2026-09-25 — репозиторий найден, «некуда коммитить» СНЯТО
|
||
|
||
Alex уточнил: «Проект на маке в developer это в доке записано. Ведь так?!» — и был прав.
|
||
|
||
🔴 **Живое место исходников плагина — `~/Developer/lampac-scripts/` (ПОД GIT), Gitea
|
||
`git_admin/lampac-scripts`.** Ранее в доке везде значился `/Users/admin/lampac-tmp/` —
|
||
это скрэтч-папка сессии 2026-09-22, **вне git** (50+ одноразовых пробников).
|
||
Именно поэтому первый заход дал «коммитить некуда»: искал в `lampac-tmp` и в `lampac/`
|
||
на Кра́кене, а репа — `lampac-scripts`. Правило из дока («скрипт, который зовёт крон на
|
||
боевой машине, обязан быть в репозитории») действовало и на плагин.
|
||
|
||
```bash
|
||
cd ~/Developer/lampac-scripts
|
||
cp .../build_plugin.py scripts/build_plugin.py
|
||
cp .../plugin-cub.js scripts/plugin-cub.js
|
||
git add scripts/build_plugin.py scripts/plugin-cub.js
|
||
git commit -m "watchlist plugin: секции CUB только на главной экране"
|
||
git push origin main
|
||
```
|
||
|
||
**Коммит `5e55a47`** — «watchlist plugin: секции CUB только на главной экране»,
|
||
2 files changed, 526 insertions(+). Запушено: `53e5803..5e55a47 main -> main` →
|
||
`https://git.mallexxx.duckdns.org/git_admin/lampac-scripts.git`.
|
||
|
||
Репы на Кра́кене (проверено `find`): только `docker/`, `obsidian/`, `watchlist-sync/`.
|
||
В `lampac/` git НЕТ — боевой `plugin.js` версионируется только через репу
|
||
`lampac-scripts` (файл кладётся в неё и оттуда заливается `scp`).
|
||
|
||
⚠️ Бэкапы боевого файла (`plugin.js.bak-*`) закрыты `.gitignore` репы (`*.bak*`) —
|
||
в git не попадают, остаются локальной страховкой рядом с файлом.
|
||
|
||
#### ⚠️ Замер «плагин не загрузился» — это была гонка, а не поломка
|
||
|
||
Сразу после `luna-send`-перезапуска проба дала:
|
||
|
||
```
|
||
ready = false plugin = false patch = false
|
||
act_url = ERR: Cannot read property 'url' of undefined
|
||
cats = NOMANIFEST
|
||
```
|
||
|
||
Выглядело как «наш плагин вообще не доехал» (`window.plugin_catalog` ставит сам
|
||
`startPlugin()`). **Фактически это был замер раньше, чем Lampa дошла до главной** —
|
||
через несколько секунд те же пробы дали `ready=true, plugin=true, patch=true`.
|
||
Также цель CDP на этот период имела `title=""` и `url=about:blank` — Lampa грузилась.
|
||
|
||
🔴 **ПРАВИЛО:** после перезапуска Lampa на ТВ **не верить первому замеру**. Либо
|
||
опрашивать `ready`, пока не станет `true`, либо искать CDP-цель по `url`-подстроке
|
||
`com.lampac.tv`, а не по `title == "Lampac"` (в момент загрузки `title` пустой,
|
||
и скрипт молча не находит цель). Alex на паузу без проверки отреагировал резко:
|
||
«Чё нахуй за слип 20?!» — **не спать, а опрашивать состояние.**
|
||
|
||
---
|
||
|
||
### Инструменты диагностики ТВ, написанные в этой сессии (переиспользовать)
|
||
|
||
| Скрипт | Что делает |
|
||
|---|---|
|
||
| `/Users/admin/lampac-tmp/wl_console.py` | CDP-клиент: сбор `console`-логов Lampa за 12 с + снимок состояния (`act`, `patch`, `menu`, `pending`, `src`) |
|
||
| `/tmp/wl_probe2..5.py` | точечные пробы структуры `Activity.active()` (ключи, типы, значения) |
|
||
|
||
🔴 **Питфоллы CDP-проб, найденные на живом ТВ:**
|
||
- `JSON.stringify(Lampa.Activity.active())` → **`TypeError: Converting circular structure
|
||
to JSON`**. Возвращать поля по одному или собирать строку вручную.
|
||
- Обращаться к отсутствующему полю в `Runtime.evaluate` **без** обёртки — CDP отвечает
|
||
без `result.value`, и выглядит это как «все пробы пустые» (первый прогон дал девять
|
||
`<none>` подряд). Каждую пробу оборачивать в `String(...)` / `typeof ...` и проверять
|
||
`subtype == "error"`.
|
||
- `Log.enable` + `Runtime.enable` дают **0 строк** лога, если Lampa уже загрузилась —
|
||
консольные сообщения, напечатанные до подключения, не отдаются. Логи читать СРАЗУ
|
||
после перезапуска приложения, либо подтверждать состояние пробами, а не логами.
|
||
|
||
### Открытые шаги (для следующей сессии)
|
||
|
||
1. ✅ **ПРИЁМКА ПРОЙДЕНА 2026-09-25** — фильтр `wlOnMain()` работает, фид виден только
|
||
на главной. Доказательство: на экране `movie` секций в DOM нет при заполненном `menu`.
|
||
2. 🔄 **ПРЕДЗАГРУЗКА `__wl_menu`** — ВЫПОЛНЕНА, но НЕ ЗАКРЫТА: см. «ТРЕТИЙ ФИКС»
|
||
ниже. Требуется ещё один заход (фильтр по `component` + вставка при возврате).
|
||
3. **Диагностировать 66 с на карточке** — `docker logs lampac` + трейс `CardController`.
|
||
Запрос: `GET /catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie`
|
||
→ 66 118 мс, 939 Б.
|
||
4. **Matcher-parity** (главный блокер с 2026-09-24, см. ниже) — всё ещё не закрыт.
|
||
5. ✅ **Версионирование плагина — РЕШЕНО:** `lampac-scripts/scripts/`, коммит `5e55a47`.
|
||
Развилка «`lampac_plugin/` в `watchlist-sync` vs бэкапы» **закрыта**: плагин
|
||
принадлежит `lampac-scripts` (там же `gen_watchlist.py`, `deploy_lampac.py`).
|
||
|
||
---
|
||
|
||
## 🔴🔴 2026-09-25 (финал): ТРЕТИЙ ФИКС — фильтр по `component` + `wlPrefetch()`
|
||
|
||
### Приёмка Alex частичная: два новых симптома
|
||
|
||
Alex после заливки `wlPrefetch`:
|
||
|
||
> «Так, хорошо, Сейчас при загрузке приложения сработало, коммить.
|
||
> Но когда я с главной ушел в раздел фильмы там снова были watchlist разделы
|
||
> А когда вернулся на главную опять разделы watchlist не сверху»
|
||
|
||
То есть: **при холодном старте — правильно** (секции сверху, фильтр держит), но
|
||
навигация ломает обе части: уход в «Фильмы» → секции появились; возврат на главную →
|
||
секции не сверху.
|
||
|
||
### 🔴 ГЛАВНАЯ НАХОДКА: на главной `url` ПУСТОЙ, а `component === 'main'`
|
||
|
||
Замер на живой Lampa (CDP) в момент, когда открыта именно главная
|
||
(`?component=main&source=cub&page=1`):
|
||
|
||
```
|
||
active_full = url= | title=Главная - CUB | component=main | source=cub | page=1 | params=obj | activity=obj
|
||
act_url_direct = "" ← url ПУСТОЙ, а не 'main'
|
||
act_act_url = undefined ← вложенный activity.url тоже пуст
|
||
menu = Новинки,Фильмы,Мультфильмы,Аниме
|
||
cats = NOMANIFEST
|
||
```
|
||
|
||
🔴 **Вывод: проверка `url === 'main'` НЕ РАБОТАЕТ ВООБЩЕ.** Она не срабатывала ни разу —
|
||
её проходила только смягчающая ветка `if (!act) { if (!catalogs) return true; }`.
|
||
Как только `Manifest.catalogs` заполнится, **фильтр начал бы резать саму главную**.
|
||
Это была мина, а не рабочее условие.
|
||
|
||
**Правильное поле — `component`:** главная `→ 'main'`, категория `→ 'category_full'` /
|
||
`'category'`. Оно стабильно на обоих экранах, в отличие от `url`.
|
||
|
||
```js
|
||
function wlComp(a) {
|
||
if (!a) return '';
|
||
if (a.component) return String(a.component);
|
||
if (a.activity && a.activity.component) return String(a.activity.component);
|
||
return '';
|
||
}
|
||
// в wlOnMain(): if (comp === 'main') return true;
|
||
// смягчающая ветка: if (!comp) { пустые catalogs → true }
|
||
```
|
||
|
||
### Замер, доказавший что секции ВСЁ-ТАКИ вставлялись
|
||
|
||
```
|
||
cat_header = Сейчас смотрят | Рекомендуем посмотреть | Последнее добавление | Огонь! |
|
||
В высоком качестве | Новинки | Фильмы | Мультфильмы | Аниме | Фэнтези | ...
|
||
↑↑↑↑↑↑↑↑ наши 4 секции — ЕСТЬ, но ПОСЛЕ родных
|
||
wl_done = false
|
||
```
|
||
|
||
То есть на главной секции присутствовали, но стояли **после** «В высоком качестве» —
|
||
ровно тот симптом «сильно после первых секций», который Alex и называл.
|
||
|
||
### `wlPrefetch()` — предзагрузка меню при старте
|
||
|
||
```js
|
||
function wlPrefetch() {
|
||
if (window.__wl_prefetched) return;
|
||
window.__wl_prefetched = true;
|
||
wlMenu(function (m) { console.log('[watchlist] меню предзагружено: ' + Object.keys(m || {}).join(',')); });
|
||
}
|
||
// в startPlugin(), рядом с wlPatchPartNext():
|
||
// if(window.appready){wlPatchPartNext();wlPrefetch();} else { … 'ready' → wlPatchPartNext();wlPrefetch(); }
|
||
```
|
||
|
||
`/catalog` тянется **параллельно** `loadCatalogs()`, чтобы `__wl_menu` лежал в памяти
|
||
к моменту первой отрисовки главной. Меряно: `menu` заполнен при
|
||
`Manifest.catalogs = NOMANIFEST` → **на `loadCatalogs()` полагаться нельзя**, нужен
|
||
независимый запрос.
|
||
|
||
### Состояние сборки и деплоя
|
||
|
||
```
|
||
python3 build_plugin.py → 14/14 инвариантов, 18947 байт
|
||
node --check plugin-cub.js → SYNTAX OK
|
||
```
|
||
|
||
**Инварианты (14):** `INJECT=1`, `partNext def=1`, `wlInject def=1`,
|
||
`unshift (не push)=1`, `push в INJECT=0`, `startPlugin=1`,
|
||
`нет network.silent в INJECT=0`, **`wlOnMain def=1`**, **`wlComp def=1`**,
|
||
**`wlComp читает component=1`**, **`wlOnMain guard=1`**, **`wlPrefetch def=1`**,
|
||
**`wlPrefetch call=3`**. Инварианты `wlUrl def` / `wlUrl верхний уровень` — **убраны**
|
||
вместе с `wlUrl()` (поле оказалось не тем).
|
||
|
||
**Деплой выполнен:** бэкап `catalog/plugin.js.bak-before-comp-<ts>` →
|
||
`scp` (18947 Б) → проверка в контейнере
|
||
`docker exec lampac grep -c "wlPrefetch\|wlComp" /lampac/module/Catalog/plugin.js` → **4**
|
||
→ рестарт Lampa на ТВ через `luna-send` (`--- закрываю --- --- запускаю --- --- готово ---`).
|
||
|
||
### ✅ ОБА СИМПТОМА РАЗОБРАНЫ ЗАМЕРОМ — ЧЕТВЁРТЫЙ ФИКС (см. раздел ниже)
|
||
|
||
Гипотезы из третьего фикса **опровергнуты замером**. Переход
|
||
`main → category_full → main` даёт **ПУСТОЙ трейс `partNext`** — он не вызывается
|
||
на этих переходах вообще, Lampa переиспользует отрисованный пул. Фильтр по
|
||
`component` при этом **работает** (`main` → пропуск, `category_full` → рез).
|
||
|
||
Полный разбор, код фикса и статус деплоя — в разделе
|
||
«🔴 РЕШЕНИЕ НАЙДЕНО ЗАМЕРОМ: хук на `activity` (ЧЕТВЁРТЫЙ ФИКС 2026-09-25)».
|
||
|
||
**Метод замера, который это дал** (`/tmp/wl_nav.py`): обернуть `Lampa.Api.partNext`
|
||
своим трейсом (`window.__wl_trace.push(component+'|url='+url+'|n='+parts.length)`),
|
||
затем дёрнуть `Lampa.Router.call('category_full',{url:'movie',...})`, снять снапшот
|
||
`Activity.active()`, вернуться, снять трейс. **Трейс пустой = `partNext` не зовётся** —
|
||
это и был ответ.
|
||
|
||
🔴 **Питфолл метода:** `Lampa.Router.call('main')` как «вернуться на главную»
|
||
**не работает** (`ERR: Cannot read property 'url' of undefined`). Возврат на главную
|
||
через Router не эмулируется — навигацию по экранам Alex проверяет пультом.
|
||
|
||
**Правило:** `git commit` через `ssh`/длинную команду может уйти в таймаут апрува —
|
||
подтверждение Alex обязательно, повторять команду автоматически нельзя.
|
||
|
||
**Правило диагностики этой сессии:** не читать `%{size_download}` как «размер фида»
|
||
(это gzip) — брать `ls -la` или `Accept-Encoding: identity`. И — `ifconfig` на Кра́кене
|
||
отсутствует: каждая ssh-команда печатает `bash: line 1: ifconfig: command not found`
|
||
безвредным шумом, это НЕ ошибка команды.
|
||
|
||
**🔴 Питфолл инструментов этой сессии:** `execute_code` падал на `import`
|
||
(`No module named 'psutil'`) — расследование и правки сделаны через `terminal` +
|
||
`read_file` + `patch`, БЕЗ python-скриптов. `scp` файла `wwwroot/catalog.js` не нашёл:
|
||
такого пути НЕТ, `plugin.js` живёт в `lampac/catalog/plugin.js` и монтируется в
|
||
`/lampac/module/Catalog/plugin.js`. Alex отдельно и дважды требовал результат
|
||
расследования (не план, не вопрос): «задача блядь поставлена», «хули ты творишь. ты
|
||
блядь не можешь в код проекта посмотреть и в доки и все выяснить?» — **сначала читать
|
||
код проекта и доки, потом отвечать.**
|
||
|
||
---
|
||
|
||
## 🔴 ЧЕТВЁРТЫЙ ФИКС 2026-09-25: хук на `activity` (решение найдено замером)
|
||
|
||
**Симптомы Alex после третьего фикса:**
|
||
> «Так, хорошо, Сейчас при загрузке приложения сработало, коммить.
|
||
> Но когда я с главной ушел в раздел фильмы там снова были watchlist разделы
|
||
> А когда вернулся на главную опять разделы watchlist не сверху»
|
||
|
||
При холодном старте — правильно; навигация ломала обе части.
|
||
|
||
### Замер (CDP, живая Lampa): трейс `partNext` + значения полей
|
||
|
||
Скрипт `/tmp/wl_nav.py`: оборачивает `Lampa.Api.partNext` своим трейсом, затем
|
||
`Lampa.Router.call('category_full',{url:'movie',...})`, снапшот `Activity.active()`,
|
||
возврат, чтение трейса.
|
||
|
||
```
|
||
ЩАС(главная?) | component=main url= title=Главная - CUB
|
||
-> category_full {url:'movie'}
|
||
ПОСЛЕ Фильмов | component=category_full url=movie title=Фильмы
|
||
ПОСЛЕ возврата| component=category_full url=movie
|
||
ТРЕЙС: [] ← partNext НЕ ВЫЗЫВАЛСЯ НИ РАЗУ
|
||
```
|
||
|
||
| Экран | `component` | `url` |
|
||
|---|---|---|
|
||
| Главная | **`main`** | `` (пусто) |
|
||
| Фильмы | **`category_full`** | `movie` |
|
||
|
||
🔴 **Фильтр по `component` РАБОТАЕТ** — `main` пропускает, `category_full` режет.
|
||
Симптом «в Фильмах снова секции» **не от от фильтра**: переход
|
||
`main → category_full → main` даёт **ПУСТОЙ трейс `partNext`** — на этих переходах он
|
||
не вызывается вообще, Lampa **переиспользует уже отрисованный пул**.
|
||
|
||
Отсюда оба симптома одним объяснением:
|
||
1. **В «Фильмах» видны секции** — это те же отрисованные элементы; Lampa не
|
||
перерисовала страницу при переходе.
|
||
2. **При возврате они не сверху** — повторного `wlInject` не было, `partNext` не звался.
|
||
|
||
🔴 **Вывод: опираться на `partNext` как на ЕДИНСТВЕННУЮ точку входа НЕЛЬЗЯ.**
|
||
|
||
🔴 **Питфолл метода:** `Lampa.Router.call('main')` как «вернуться на главную» **не
|
||
работает** (`ERR: Cannot read property 'url' of undefined`). Навигацию по экранам
|
||
проверяет Alex пультом, программно её не воспроизвести.
|
||
|
||
### Фикс — `wlWatchActivity()`
|
||
|
||
Хук на `Lampa.Listener.follow('activity')`. На старте activity с `component='main'`
|
||
сбрасывается `window.__wl_done = false` — следующий `partNext` вставляет секции заново
|
||
(в голову, через `unshift`):
|
||
|
||
```js
|
||
function wlWatchActivity() {
|
||
if (window.__wl_activity_hook) return;
|
||
window.__wl_activity_hook = true;
|
||
Lampa.Listener.follow('activity', function (e) {
|
||
if (e.type !== 'start') return;
|
||
if (wlComp({component: e.component}) === 'main') {
|
||
window.__wl_done = false; // ← снять флаг вставки
|
||
if (!window.__wl_menu || !Object.keys(window.__wl_menu).length) wlPrefetch();
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
Плюс **`wlPrefetch()` перестал быть одноразовым** — убран гейт `__wl_prefetched`,
|
||
меню обновляется при возврате на главную. Вызов в `startPlugin()`:
|
||
`wlPatchPartNext();wlPrefetch();wlWatchActivity();`.
|
||
|
||
### Сборка и деплой (выполнено)
|
||
|
||
```
|
||
python3 build_plugin.py → 17/17 инвариантов, 19773 байт
|
||
node --check plugin-cub.js → SYNTAX OK
|
||
docker exec lampac grep -c wlWatchActivity /lampac/module/Catalog/plugin.js → 3
|
||
бэкап: catalog/plugin.js.bak-before-activityhook-<ts>
|
||
рестарт Lampa на ТВ: --- закрываю --- --- запускаю --- --- готово ---
|
||
```
|
||
|
||
**Инварианты (17 всего).** Добавлены `wlWatchActivity def=1`,
|
||
`wlWatchActivity call` — считается по строке `wlPrefetch();wlWatchActivity();`,
|
||
🔴 **НЕ по `wlWatchActivity()`** (иначе определение ловится как вызов, счётчик врёт:
|
||
первый прогон дал 3 вместо 1 → инвариант упал), `activity hook сбрасывает флаг`.
|
||
|
||
### Приёмка (ждёт Alex) — полный цикл, а не только первый экран
|
||
|
||
1. главная → 4 секции Watchlist сверху, **БЕЗ дублей внизу**
|
||
2. Фильмы → документальные → секций Watchlist нет
|
||
3. **назад на главную → секции СНОВА сверху, снова без дублей** ← пункт, который ломался
|
||
|
||
✅ **Коммиты СДЕЛАНЫ и запушены** (в `~/Developer/lampac-scripts`, Gitea):
|
||
- `5e55a47` — «секции CUB только на главной экране» (фильтр `wlOnMain`)
|
||
- `3a35475` — «фильтр по component + предзагрузка меню + хук на activity»
|
||
|
||
🔴 **Правки обязаны попадать в репу ДО доклада.** В этой сессии `5e55a47` был
|
||
закоммичен, а работа по фильтру по `component` осталась незакоммиченной, и Alex
|
||
справедливо спросил «Коммит ты сделал текущей работы я надеюсь». Порядок:
|
||
`build_plugin.py` правится → собирается → **копируется в `scripts/` → коммит → push**,
|
||
и только потом заливка и приёмка. Файлы в `/Users/admin/lampac-tmp/` — НЕ источник.
|
||
|
||
🔴 **Правило процесса:** Alex не принимает статус-репорты и вопросы вместо результата
|
||
(«Ты заебал. Ты блядь задачу решаешь или мозг мне ебешь?!», «Ты делаешь блядь или
|
||
опять залип», «Ты все починил? Проверять?») — **доделать и доложить один раз**.
|
||
Замер — часть работы, а не отдельная задача на согласование.
|
||
|
||
🔴 **Правило проверки перед докладом.** Дважды за сессию доклад «готово, проверяй»
|
||
был **ложным**: правка физически не доезжала до ТВ (неработающий рестарт приложения).
|
||
Перед словом «готово» обязателен замер уникального маркера сборки на ТВ. Alex ловил
|
||
это сам («С чего ты взял что лампа перезапущена?», «Ты где его взял? Эта ебанина
|
||
где-то записана?») — это не должно повторяться.
|
||
|
||
---
|
||
|
||
## 🔴 2026-09-24 (вечер): ПЕРЕПИСЫВАНИЕ — один sync-скрипт вместо отдельного генератора
|
||
|
||
**Требование Alex:** «перепиши по нормальному чтобы не отдельный скрипт на лампу был
|
||
а один sync скрипт и писал сразу lampa feed… обнови синк скрипт чтобы писал фид вместо
|
||
добавления закачек в arr, а отдельный lampac script удали».
|
||
|
||
**Причина (найдено в этой сессии).** `gen_watchlist.py` (Lampac, 878 строк) — ВТОРОЙ,
|
||
независимый матчер TMDB: он не читает ни `.resolve_cache.json`, ни маркеры ✅/🗓️/❓.
|
||
Годами его связывал с резолвером Radarr неявно (матч по названию → те же версии фильмов,
|
||
что выбрал `resolve`). После удаления контента из arr 2026-09-24 мост исчез, и каталог
|
||
впервые матчил вслепую → 3 карточки сменили фильм (Олдбой 2003→2013 ремейк,
|
||
Unbroken 2003→Несломленный 2014, Shogun 1968→Убийца сёгуна 1980). Лечим не годами
|
||
в `.md`, а слиянием матчеров.
|
||
|
||
**Статус: ✅ РЕАЛИЗОВАНО 2026-09-24 (вечер).** Отказ от отдельного `feed.py`/CLI-шага —
|
||
Alex уточнил требование: «это же чистая доработка sync-down?» → **`sync-down` сам пишет фид**.
|
||
Отдельного шага `feed` в CLI и в nightly НЕ появилось.
|
||
|
||
| Что | Как сделано |
|
||
|---|---|
|
||
| Новый `watchlist_sync/sync_down.py` | ~380 строк вместо 195: `resolve` → `wwwroot/watchlist.json` + `watchlist-<cat>.json` + `cards/card-<movie-NNN>.json` + `cards.json` + `missing.json`; опционально ❓-синк |
|
||
| CLI | `sync-down [--apply] [--feed-root]`; `--batch` оставлен deprecated (в фид идёт весь список) |
|
||
| Пути | `--feed-root` → env `LAMPAC_ROOT` → `<repo>/../lampac` (fallback для Мак-чекаута). Хардкода UUID-путей в коде нет |
|
||
| `arr.py` | 🔴 **УДАЛЁН** (`git rm`, коммит `a524f5d`) — история сохранена в гите. Бэкапы НЕ делались (решение Alex: «в истории гита останется») |
|
||
| ⬇️ | **не ставится больше нигде.** Alex: «⬇️ больше ставить не надо, ведь закачки нет». `mark_queued` в `parser.py` оставлен для исторических строк |
|
||
| Коммит | `a524f5d` — «sync-down: build Lampa feed instead of adding to arr; drop arr.py» |
|
||
|
||
**🔴 Три открытия при переносе (стоили трёх итераций — не повторять):**
|
||
|
||
1. **Фильтры рейтинга/ужасов в фиде НЕ НУЖНЫ.** Я притащил в фид правила закачки
|
||
(`rating >= 6.0`, horror-skip) — фид упал до 268 позиций из 392. Это правила
|
||
**Radarr** («не качаем то, что не смотрим»), а каталог — не очередь закачки:
|
||
старый генератор отдавал horror тоже (Вивариум, Субстанция, Ужасающий, Зубастики
|
||
есть в живом фиде). В `sync_down.py` оставлен явный комментарий с запретом
|
||
возвращать эти фильтры.
|
||
|
||
2. **`item.ambiguous` (❓) НЕ исключает позицию из фида.** ❓ означает «название в `.md`
|
||
неточное, пригодился бы год», а НЕ «нерезолвлено». Резолвер для таких всё равно
|
||
отдаёт кандидата, и старый генератор их публиковал (Альфа, Кибердеревня, Орвилл,
|
||
Ножницы — все с постерами в живом фиде). Из фида исключают только: нет `tmdb_id`
|
||
и `result.year_ambiguous` (🗓️ — реально два разных фильма).
|
||
|
||
3. **Фид должен брать лучшего кандидата, а не отказываться.** `resolve()` при
|
||
неоднозначности возвращает `status="ambiguous"`, `candidate=None`, но топ-3 лежат
|
||
в `all_candidates` — среди них верный. Старый генератор брал первый результат TMDB.
|
||
Если строить фид строго по `result.status == "ok"`, теряется **54 позиции**
|
||
(девять, Мементо, Babylon, Orville, Матрица-класс названий: Казино, Сияние, Оно,
|
||
Отступники, Бешеные псы…).
|
||
✅ **РЕШЕНО 2026-09-24:** Alex — «бери лучшего кандидата и пиши вопрос». Фид берёт
|
||
`all_candidates[0]`, а название уходит в `missing` → ❓ в `.md` (год, добавленный
|
||
после ❓, пиннит позицию на следующем прогоне). Реализовано в `sync_down.py`.
|
||
|
||
**🔴 ГЛАВНЫЙ БЛОКЕР, вскрытый переносом (не закрыт):** резолвер проекта матчит
|
||
**хуже** старого генератора. Сверка по TMDB-номеру (не по названию — в фиде имена
|
||
из TMDB, в `.md` из строки, сравнение по имени бесполезно):
|
||
|
||
```
|
||
было в живом фиде, в новом с ДРУГИМ tmdb_id: 86 позиций
|
||
примеры: Злая: Часть 2, Разделение, Дракула, Ван Хельсинг, Судья Дредд, Дюна-класс
|
||
плюс мусорные матчи по кривым строкам: «[Убийственная вечеринка] (2025)» → фильм 2007
|
||
```
|
||
|
||
**Причина:** в старом генераторе 4 ступени резолва — Radarr → TMDB → **КП-по-номеру
|
||
из ссылки `kinopoisk.ru/film/N/`** → КП-по-названию. Самая надёжная ступень
|
||
(номер из ссылки — машинный ключ, не зависит от переименований) в `resolver.py`
|
||
**отсутствует вообще**. До переноса это маскировал Radarr-фастпатч; после удаления
|
||
контента arr маскировка исчезла и расхождение вылезло.
|
||
|
||
**Варианты (ждёт решения Alex):**
|
||
- **А (рекомендован):** перенести в `sync_down`/`resolver` ступень «КП-по-номеру из
|
||
ссылки» — код разобран и лежит в бэкапе `gen_watchlist.py` (`kp_search_by_id` +
|
||
`_docs_to_tmdb` с `_by_id`). ~30 мин, проверяется тем же diff по tmdb_id.
|
||
- **Б:** выкатить как есть и вычистить 86 расхождений руками — вернутся после прогона.
|
||
|
||
**Замеры dry-run по ходу работы:** 268 (с фильтрами) → 332 (без фильтров, `ambiguous`
|
||
исключается) → **397** (`all_candidates[0]` + ❓; больше 392, потому что старый
|
||
генератор терял часть позиций на Radarr-фастпатче и ❓-строках).
|
||
На живой фид НЕ выкатывалось: сначала matcher-parity.
|
||
|
||
**Реперные метрики живого фида на 2026-09-24** (для сверки после переноса):
|
||
```
|
||
каналы: new 32 / films 346 / cartoons 7 / anime 7 → карточек 392
|
||
источники: Radarr 0, TMDB 371, автопоиск по названию 17,
|
||
по номеру из ссылки 4, без TMDB 30, постеров 390
|
||
```
|
||
|
||
🟡 **Рабочее правило (Alex):** код писать сразу в проекте на Маке
|
||
(`~/Developer/watchlist-sync/`), `/tmp` — только для чтения чужих файлов и одноразовых
|
||
выборок. Мак-репа и Кракен держатся на одном коммите (`4266e37` на момент старта).
|
||
|
||
**Границы (что не трогаем):** `catalog/sites/watchlist.yaml` (контракт Lampac:
|
||
`plugin: watchlist`, категории, `card_parse`/`card_args` с `format`-выражениями),
|
||
`catalog/plugin.js` (инжект секций в CUB через `Api.partNext`), структура
|
||
`cards/card-movie-NNN.json`. Механика `missing.json` → ❓ осталась, переехала внутрь
|
||
`sync_down.py` (`_sync_question_marks`, двусторонняя — снимает протухший ❓).
|
||
|
||
**🔴 Незакрытое на конец сессии 2026-09-24 (для следующей сессии):**
|
||
1. **Matcher-parity (ГЛАВНОЕ)** — см. «ГЛАВНЫЙ БЛОКЕР» выше: перенести ступень
|
||
«КП-по-номеру из ссылки» в резолв фида (вариант А) либо вычистить 86 расхождений
|
||
руками (вариант Б). Без этого выкат на живой фид покажет не те фильмы на ~86 позициях.
|
||
2. Возврат `sync-down` в `watchlist-nightly.sh` (строка 40 сейчас «ПРОПУЩЕН»;
|
||
в 02:30 крутится отдельный `watchlist-catalog.sh`) + удаление `watchlist-catalog.sh`
|
||
и строки крона `30 2 * * *`.
|
||
3. Удаление `lampac/scripts/gen_watchlist.py` (878 строк) и `mark_missing.py` (179).
|
||
🔴 НЕ удалять до закрытия п.1: оттуда берётся код `kp_search_by_id` / `_docs_to_tmdb`.
|
||
4. Прогон `--apply` на живой фид + `docker restart lampac` (сброс кэша Catalog)
|
||
+ приёмка в UI Lampa.
|
||
5. Правка `sync_down.py`: ветка «не нашлось» (`not_found`) обязана добавлять позицию
|
||
в `missing` — за это отвечает `_sync_question_marks` (двусторонний ❓).
|
||
|
||
---
|
||
|
||
> Реализовано 2026-09-22. План был переписан после разведки кода модуля Catalog.
|
||
> Базовое развёртывание — [[personal/plans/lampac-kraken]] (шаги 1–6 закрыты).
|
||
>
|
||
> 🔴 **АРХИТЕКТУРА ПРИЗНАНА НЕВЕРНОЙ 2026-09-22 — переписывается.**
|
||
> Сделано `menu` (подмена родных разделов) вместо `main` (секции СВЕРХУ).
|
||
> Плюс источник данных — парсинг текстового `.md` вместо **Radarr API**, где уже
|
||
> есть TMDB ID и постеры. Alex: «Я предполагал что будет отображаться то что и раньше
|
||
> (список cub) но В НЕГО будет сверху добавлено несколько секций в которых будут
|
||
> РЕАЛЬНЫЕ ЭЛЕМЕНТЫ - а не сгенерируют пустые заглушки!»
|
||
> См. «🔴 АРХИТЕКТУРНАЯ ОШИБКА» — читать ПЕРВЫМ.
|
||
>
|
||
> **Итог одной строкой:** серверная часть каталога работает полностью — «Мой список»
|
||
> первым в `/catalog`, 4 категории (403 позиции, 348 постеров), **карточка по клику
|
||
> отдаёт все поля: 352/352 с числовым `tmdb_id`**. Родной CUB не тронут.
|
||
> Осталась приёмка глазами на ТВ.
|
||
>
|
||
> **Обновление 2026-09-22 (четвёртая сессия): 🔴🔴 НАЙДЕНА ИСТИННАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО» — КИРИЛЛИЦА В КЛЮЧЕ КАТАЛОГА.**
|
||
> Все предыдущие фиксы (числовой `tmdb_id`, права, macOS-tar) были **не той причиной**.
|
||
> `tcpdump` на Кра́кене при клике на ТВ показал реальный запрос:
|
||
> `GET /catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=... → 400 Bad Request`.
|
||
> **`plugin=Мой список`** — кириллица. `ModInit.goInit` строки 51-52 чистит имя регуляркой
|
||
> `[^a-z0-9\-]`, то есть **вырезает всю кириллицу целиком** → `""` → `File.Exists("sites/.yaml")` = false →
|
||
> fallback `FindSiteByDisplayName("")` тоже мимо (сравнивает уже с пустой строкой) → `null` →
|
||
> `BadRequest("init not found")` → **400** → «Здесь пусто».
|
||
> **ФИКС:** явный `plugin: watchlist` в `watchlist.yaml` (переопределяет ключ по
|
||
> `ApiController.cs:136`); `displayname: Мой список` остаётся надписью в UI. Ключ каталога стал `watchlist`.
|
||
> Проверено: `plugin=Мой список` → **400**, `plugin=watchlist` → **200** с полными данными.
|
||
> 🔴 **Урок диагностики:** проверять карточку curl'ом надо ТЕМ ЖЕ ключом, который шлёт ТВ
|
||
> (я подставлял латиницу руками → 200 → ложный вывод «проблема в клиенте»). Детали —
|
||
> [[personal/tech/lampac-deployment]] раздел «🔴🔴 ГЛАВНАЯ ПРИЧИНА».
|
||
>
|
||
> **⚠️ ЗАДАЧА СЕССИИ (требование Alex): ✅ ВЫПОЛНЕНО — СЕКЦИИ ВНЕДРЯЮТСЯ В CUB-ФИД.**
|
||
> Дословно требование: «надо блядь добавить в CUB список новые секции а не ручное
|
||
> блядь переключение!». Реализовано в `plugin-cub.js` через **обёртку
|
||
> `Lampa.Api.sources.cub.main`**: наши секции **добавляются** в массив
|
||
> `parts_data` CUB-фида (не подменяют раздел), карточкам проставляется
|
||
> `source: 'watchlist'`. Вызов — по событию `app:ready`.
|
||
> Залито, отдаётся в сборке `/catalog.js` (35566 Б). **Ожидает приёмки в UI.**
|
||
> Механика — [[personal/tech/lampac-deployment]] раздел «Внедрение секций Watchlist в CUB-фид».
|
||
>
|
||
> **Обновление 2026-09-22 (ВОСЬМАЯ СЕССИЯ): ✅✅ ЭТАП «СЕКЦИИ ВНУТРИ CUB» ЗАКРЫТ — ФИД СМЕРЖИЛСЯ.**
|
||
> Alex: «Отлично, фид смержился. Закрываем этот этап в плане.»
|
||
>
|
||
> **Корень, который держал этап** (нашёл только по коду `app.min.js` @1292588):
|
||
> ```js
|
||
> function partNext(parts, parts_limit, partLoaded, partEmpty) {
|
||
> var pieces = parts.filter(function (p) {
|
||
> return typeof p == 'function';
|
||
> }).slice(0, 0 + parts_limit); // ← limit = 6, ЖЁСТКИЙ СРЕЗ ГОЛОВЫ
|
||
> ```
|
||
> `cub.main$1` кладёт в `parts_data` **28 родных секций** и вызывает `partNext(parts, 6, …)`.
|
||
> Наш инжект делал `parts.push()` → секции уходили в **хвост** (позиции 29–32) → `slice(0, 6)`
|
||
> их отрезал. Трасса пула на живом ТВ: `before: 28 → after: 32`, в DOM — **ни одной** нашей секции.
|
||
>
|
||
> **ФИКС: `parts.unshift()` вместо `parts.push()`** — в голову пула, в срез, рисуются СВЕРХУ
|
||
> (ровно требование Alex). Порядок меню обходится **в обратном порядке**, потому что каждый
|
||
> `unshift` сдвигает предыдущие вправо.
|
||
>
|
||
> **Ложный след, потративший время:** тест `g: {wlSection: "undefined", …}` читался как
|
||
> «функции недоступны из замыкания». На самом деле инжект **внутри** замыкания плагина и
|
||
> `wlSection` виден перехватчику — проба смотрела снаружи. Доказательство: прямой вызов
|
||
> `partNext(empty,…)` дал `len: 4`. **Проверять пул ТРАССОЙ (`before/after`), а не наличием
|
||
> функций на `window`.**
|
||
>
|
||
> **Приёмка (серверная часть): 18/18.** `/catalog.js` 33550 Б с `unshift`; `/catalog` →
|
||
> ключ `watchlist`, menu 4 категории; список: new 33/30 постеров, films 360/308/312 ID,
|
||
> cartoons 3/3, anime 7/7; карточка `tmdb_id: 424781` числом; регресс-тест `plugin=Мой список`
|
||
> → **400** (кириллица по-прежнему не проходит).
|
||
>
|
||
> **Артефакты:** `build_plugin.py` (+инварианты `wlInject def`, `unshift (не push)`,
|
||
> `push в INJECT (должно 0)`), `verify_unshift.py` (проверка фикса в рантайме БЕЗ деплоя),
|
||
> `full_check.py` (полный цикл сервер+ТВ), `tv_accept.py`, `diag_trace.py`, `diag_calls.py`.
|
||
> Бэкапы: `plugin.js.bak-20260922-165613` (последний рабочий), `plugin-cub.js.bak-*`.
|
||
>
|
||
> 🔴 **Питфолл рантайм-проверки:** повторная загрузка `/catalog.js` скрипт-тегом НЕ перезапускает
|
||
> `startPlugin` (флаг `window.plugin_catalog` уже стоит) → `patched: false`. Для быстрой итерации
|
||
> без деплоя оборачивать `Lampa.Api.partNext` вручную с `unshift` (`verify_unshift.py`).
|
||
> `location.reload()` на webOS догружает новый `catalog.js` **дольше 30 с** — ждать до 120 с.
|
||
|
||
> **Обновление 2026-09-22 (седьмая сессия): 🔴🔴 ПРИЧИНА «В CUB НИХУЯ НЕ ПОМЕНЯЛОСЬ» — `Manifest` НЕДОСТУПЕН В ОБЁРТКЕ.**
|
||
> Alex: «Ты вообще нихуя не поменял!!». Проверка через **CDP-пробу на живом ТВ** подтвердила
|
||
> его правоту: обёртка `Api.sources.cub.main` стояла (`__wl_injected: true`), но вызов
|
||
> `cub.main({})` возвращал **4 родные секции** и **ни одной нашей**.
|
||
> **Корень:** первая версия инжекта читала `Manifest.catalogs['watchlist']` напрямую, а
|
||
> `Manifest` объявлен **локально в замыкании плагина** → `ReferenceError: Manifest is not defined`
|
||
> → `catch` → молча отдавался родной фид. **Ошибки нет ни в UI, ни в логах.**
|
||
> **Фикс:** `window.__wl_menu` (заполняется в `loadCatalogs`) + `wlEnsureMenu()` с фолбэком
|
||
> на запрос `/catalog`. Переписан сборщик: **`build_plugin.py`** (детерминированный,
|
||
> с инвариантами `INJECT=1`/`def=1`/`startPlugin=1`, `node --check` + баланс скобок),
|
||
> вместо неидемпотентного `patch_cub_inject.py` (он вставлял INJECT дважды и ломал скобки).
|
||
> Залито (сборка **35440 Б**), **ждёт перезапуска Lampa на ТВ** — `docker restart lampac`
|
||
> НЕ перезагружает плагин в памяти ТВ.
|
||
> Механика + CDP-пробы — [[personal/tech/lampac-deployment]] раздел
|
||
> «🔴🔴 ШЕСТАЯ СЕССИЯ: ИСТИННАЯ ПРИЧИНА…».
|
||
|
||
> **Обновление 2026-09-22 (пятая сессия): ✅✅ ПРИЁМКА ALEX ПРОЙДЕНА — ФИЛЬМЫ ОТКРЫВАЮТСЯ И ПРОИГРЫВАЮТСЯ.**
|
||
> Дословно: «фильмы из списка Watchlist теперь открываются и проигрываются».
|
||
> Закрыт последний блокер — кириллический ключ плагина (`plugin: watchlist`).
|
||
> Плюс устранён питфолл `Staticache` (кэш собранного `/catalog.js` переживал
|
||
> заливку и выглядел как «патч не применяется» → `docker restart lampac`).
|
||
>
|
||
> **Осталось (2026-09-22, конец СЕДЬМОЙ сессии):**
|
||
> 0. ✅ **ЗАКРЫТО 2026-09-22 (восьмая сессия):** `plugin-cub.js` залит на Кра́кен,
|
||
> фикс `unshift` вместо `push` — **фид смержился**, секции Watchlist рисуются
|
||
> внутри главной CUB рядом с родными. См. блок «ВОСЬМАЯ СЕССИЯ» выше.
|
||
> 1. **5 фильмов с Кинопоиск-ID** (`film/NNNNN/`) — «Рассекая волны: экспедиция „Лас Бальсас“»,
|
||
> «Хамнет», «Эхопраксия», «Цена вершины», «Астрал. Могильный ритуал 2», «О добре и зле»,
|
||
> WeWork (2024).
|
||
> TMDB не нашёл (2025 год) → нет карточки и постера → клик даёт «здесь пусто».
|
||
> **Нужен выбор Alex:** заглушка с кнопкой поиска торрента либо дорисовать `tmdb_id`.
|
||
> 2. **~51 название без постера** — проблема ЗАПИСИ В СПИСКЕ, не скрипта:
|
||
> `Плохие парни 1/2` (ДВА фильма одной строкой), `Солярис Тарковский` (уточнение
|
||
> режиссёра), `Самолетом, поездом, машиной (1987)` (русский аналог).
|
||
> Генератор такое угадать не может — **правится только список руками**.
|
||
>
|
||
> **Обновление 2026-09-22 (четвёртая сессия, ранее): ✅ ЧИСЛОВОЙ `tmdb_id` В КАРТОЧКАХ.**
|
||
> 352 из 352 карточек отдают `tmdb_id` числом, `source`, `runtime`, `vote_average`, жанры.
|
||
> Механика: `ModInit.cs:285 nodeValue` возвращает любой `JValue` СТРОКОЙ → в `card_args`
|
||
> нужен `format: "long.TryParse(value, out long v) ? v : 0"` (`format` = CSharpEval,
|
||
> `CatalogNodeValue(value, host)`). Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ и
|
||
> переживает `docker restart` — после правки данных обязателен рестарт И проверка на
|
||
> НЕзакэшированном uri. Детали — [[personal/tech/lampac-deployment]].
|
||
>
|
||
> **Обновление 2026-09-22 (третья сессия): ✅ КАРТОЧКА ПОЧИНЕНА ОКОНЧАТЕЛЬНО.**
|
||
> «Здесь пусто» давало отсутствие **числового** `tmdb_id`: `plugin.js` делает
|
||
> `if(json.tmdb_id)`, а `ModInit.cs:285` возвращает любое `JValue` СТРОКОЙ.
|
||
> Фикс — `format: "long.TryParse(...)"` в `card_args`. Плюс кэш `catalog:card:*`
|
||
> в ПАМЯТИ переживал `docker restart` и отдавал старое. См. «🔴 ФИНАЛЬНЫЙ ФИКС».
|
||
>
|
||
> **Обновление 2026-09-22 (вечер):** Alex лично нашёл источник в UI
|
||
> («остальное → основной источник → Мой список»), увидел результат и подтвердил
|
||
> диагноз: секции подменяют CUB, нет превью, элементы — сырые строки файла.
|
||
> Затем сам сформулировал целевую схему (Radarr = источник метаданных **без автоскачки**,
|
||
> просмотр через TorrServer, файлы на HDD не пишутся) → записано в **«ПЕРЕДЕЛКА 2»**.
|
||
> План работ на апруве, **ничего не выполнено**.
|
||
>
|
||
> **Обновление 2026-09-22 (поздний вечер): ✅ ШАГИ 0–4 ПЕРЕДЕЛКИ 2 ВЫПОЛНЕНЫ.**
|
||
> Автоскачка на HDD **выключена фактически**: Radarr (45→**0** monitored, очередь 6→**0**,
|
||
> RSS 30→**0**) и Sonarr (13→**0**, 19→**0**, 15→**0**). Метаданные целы — 50 фильмов,
|
||
> **46 с постерами**, 24 файла на диске не тронуты. Проверено: Transmission не сдвинулся
|
||
> ни на байт за 45 с, логи Radarr пусты. Бэкапы `radarr.db`/`sonarr.db`/`config.xml` — метка
|
||
> `20260922-123404`. См. «✅ Порядок работ» и «🔴 Питфоллы выполнения».
|
||
>
|
||
> **Обновление 2026-09-22 (ночь): 🔴 ПОПЫТКА УБРАТЬ `menu` ПРОВАЛЕНА — ГИПОТЕЗА БЫЛА НЕВЕРНА.**
|
||
> Попытка «оставить `main`, убрать `menu`» **сломала канал**: «Мой список» исчез из `/catalog`,
|
||
> `catalog/list?plugin=watchlist` отдавал пустоту. Три раунда правок YAML, три рестарта.
|
||
> **Установлено тестом:**
|
||
> - **`menu` ОБЯЗАТЕЛЕН** — он регистрирует канал, а не подменяет разделы
|
||
> (`ApiController.cs:70`: без `menu` сайт отбрасывается). Убрать нельзя.
|
||
> - **блок `main` в YAML НЕ СУЩЕСТВУЕТ** — любые формы (список/словарь) ломают парсинг,
|
||
> `goInit` → null, сайт молча пропадает. **Errors в логах нет.**
|
||
> - **`main` Catalog генерирует САМ** из `menu` — в JSON-ответе `/catalog` есть оба ключа
|
||
> с одинаковым содержимым.
|
||
> - **Подмену разделов делает `plugin.js`**, не YAML (`Listener.follow('menu')` + `e.abort()`),
|
||
> срабатывает только при выбранном источнике «Мой список».
|
||
> - **`host` + `uri` СКЛЕИВАЮТСЯ** (`ListController.cs:53`) → `uri` обязан быть относительным.
|
||
> Также найден питфолл: **явный `format` в `menu` перезагружает страницу** при выборе раздела.
|
||
>
|
||
> **Текущее состояние:** каталог **рабочий** (waтchlist.yaml откачен к рабочей форме,
|
||
> «Мой список» на месте, 4 категории 360/33/3/7). Правки 5–8 (постеры через `resolver.py`,
|
||
> `plugin.js`, `arr.py`) **не начаты** — ждут решения Alex по вопросу
|
||
> «CUB и Мой список одновременно». См. **«🔴 ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН»**.
|
||
>
|
||
> **Аудит «как это реализуемо» — ✅ ЗАВЕРШЁН 2026-09-22.** Проверено всё:
|
||
> TorrServer **на диск не пишет** (`UseDisk: false`, RAM-кэш 96 МБ) — главный вопрос закрыт;
|
||
> поиск торрентов работает через **Jackett-API самого Lampac** (отдельный Jackett не нужен);
|
||
> найдены **оба** виновника автоскачки — Radarr (45 monitored, очередь 6) и
|
||
> **Sonarr** (13 monitored, очередь 19). См. «✅ ПРОВЕРКА "как это реализуемо"».
|
||
> ⚠️ Порядок работ теперь включает Sonarr — раньше он в плане не учитывался.
|
||
>
|
||
> **Обновление 2026-09-22 (ночь, вторая сессия): 🎯 НАЙДЕНА НАСТОЯЩАЯ ПРИЧИНА «пустого каталога» + артефакты готовы.**
|
||
> `Фильмы` (первая категория в `menu.categories`) возвращала **360 элементов из `watchlist.json`**
|
||
> вместо `watchlist-films.json` — то есть игнорировала блок `format`. Отсюда «здесь пусто»:
|
||
> элементы шли с `id` = ссылка на поиск Кинопоиска и `img: null`.
|
||
> **Фикс: первая категория в `menu.categories` — это «Новинки», не «Фильмы».**
|
||
> Все 4 категории проверены: films/new/cartoons/anime → правильные файлы.
|
||
> Написаны: `gen_watchlist_v2.py` (постеры через TMDB + Radarr; первый прогон
|
||
> **330 постеров из 403**), `patch_plugin.py` (+`enumerable:true`, `node --check` чист),
|
||
> `deploy_lampac.py` (заливка + проверка одной командой), обновлённый `docker-compose.yml`
|
||
> (volume на `plugin.js`). Правка `arr.py` применена и **закоммичена `9872016`**.
|
||
> См. **«🟢 РЕШЕНИЕ: первая категория игнорирует format»** — читать ПЕРВЫМ.
|
||
>
|
||
> **Обновление 2026-09-22 (ночь, третья сессия): ✅ ДЕПЛОЙ ПРОШЁЛ, ПОСТЕРЫ И ПОИСК В UI ЕСТЬ. КАРТОЧКА В РАБОТЕ.**
|
||
> Всё залито на Кра́кен и **проверено на живом сервере**:
|
||
> - «Мой список» первый в `/catalog`, `main` = `[Новинки, Фильмы, Мультфильмы, Аниме]`;
|
||
> - **films 360 / постеров 308**, new 33/30, cartoons 3/3, anime 7/7;
|
||
> - `id` элементов = **`movie/424781`** — рабочие TMDB-карточки (не поиск Кинопоиска);
|
||
> - патч `plugin.js` **применён** (`enumerable:true`, файл 10738 Б), volume смонтирован —
|
||
> проверено `docker inspect`, переживёт обновление образа;
|
||
> - `arr.py` фикс в бою, Radarr/Sonarr monitored 0, RSS 0, очередь 0.
|
||
>
|
||
> 🔴 **ОСТАЛОСЬ ОДНО: карточка по клику.** Была HTTP 500 → сейчас **HTTP 400 `parse.node`**.
|
||
> Корень найден по коду: `CardController.Index(plugin, uri, type)` — **параметра `cat` НЕТ**
|
||
> (поэтому `{cat}` в `initUrl` пустой → `cards-.json` → 404 → 500), а **`parse.node` —
|
||
> статичный JSONPath** (`json.SelectToken(parse.node)`, строка 95), подстановок не делает,
|
||
> и `/` в пути = вложенность.
|
||
>
|
||
> **Решение дописано, но НЕ залито:** файл на каждый фильм `wwwroot/cards/card-movie-NNN.json`
|
||
> с обёрткой `{"card": {...}}`, `initUrl` собирает путь C#-выражением
|
||
> `$"{host}/cards/card-{uri.Replace("/", "-")}.json"`, `node: card`.
|
||
> `deploy_lampac.py` пока НЕ умеет лить каталог `cards/` — доклеить `scp -r`.
|
||
> См. **«🔴 КАРТОЧКА: механика card_parse»**.
|
||
|
||
> **Обновление 2026-09-22 (ночь, четвёртая сессия): ✅ КАРТОЧКА ПОЧИНЕНА. 339/339 HTTP 200. СЕРВЕРНАЯ ЧАСТЬ ЗАКРЫТА.**
|
||
> Схема «файл на фильм» залита и **проверена на живом сервере**:
|
||
> - `wwwroot/cards/card-movie-NNN.json` (345 файлов), обёртка `{"card": {...}}`,
|
||
> владелец `1000:1000`, режим `644`;
|
||
> - `card_parse.initUrl` = C# `return $"{host}/cards/card-{uri.Replace("/", "-")}.json";`,
|
||
> `node: card` (статичный — потому и нужен файл-на-фильм);
|
||
> - **все 339 карточек с TMDB-ID отдают HTTP 200**, имена читаются
|
||
> («Я буду жить», «На цепи», «Простите за беспокойство»);
|
||
> - `deploy_lampac.py` дополнен шагом **2b** (заливка каталога `cards/` через tar)
|
||
> и проверкой карточек в шаге 7.
|
||
>
|
||
> **Итог каталога:** Новинки 33/30, Фильмы 360/308, Мультфильмы 3/3, Аниме 7/7 —
|
||
> **403 позиции, 348 постеров, 352 рабочих карточки**, постеров-подложек нет.
|
||
>
|
||
> 🔴 **Три причины поломки, все устранены** (детали — «🔴 ПИТФОЛЛЫ, найденные по ходу»):
|
||
> 1. **права**: tar распаковывал от root → `root:root 600` → dotnet `Permission denied` →
|
||
> статика молча **404** (выглядит как «файла нет»);
|
||
> 2. **macOS AppleDouble**: BSD `tar` клал `._*.json` в `wwwroot` → нужен `--no-mac-metadata`;
|
||
> 3. 🔴 **ложный диагноз — потерянный час**: карточку проверял по **`/card`**, где всегда 404.
|
||
> Правильный путь — **`/catalog/card?plugin=watchlist&uri=movie/NNN`**.
|
||
>
|
||
> **ОСТАЛОСЬ: приёмка глазами на ТВ** (`192.168.1.70`): секции сверху с постерами,
|
||
> клик → карточка с описанием → воспроизведение через TorrServer.
|
||
> **Приёмка = результат виден в UI**, не «API отвечает».
|
||
>
|
||
> **Не тронуто, ждёт решения Alex:** 310 фильмов из списка, которых нет в Radarr
|
||
> (51 без постера, идут через фолбэк Кинопоиска). Без апрува не делать.
|
||
|
||
---
|
||
|
||
## 🟢 ВОСЬМАЯ СЕССИЯ 2026-09-25: ФИЛЬТР «СЕКЦИИ ТОЛЬКО НА ГЛАВНОЙ» (PLAN)
|
||
|
||
**Задача Alex:** «watchlist фид отображается не только на главной, но даже когда я
|
||
выбираю категорию например "документальные" в разделе Фильмы» + «он как будто долго
|
||
прогружается относительно основного CUB списка». Требование: **ТОЛЬКО ГЛАВНЫЙ ЭКРАН**
|
||
(белый список, не чёрный — Alex: «В смысле всё кроме? Только главный экран!»).
|
||
|
||
### Диагноз (по коду, замеры)
|
||
|
||
1. **Причина «на любом экране».** `plugin-cub.js:164` — `wlPatchPartNext` вставляет
|
||
секции БЕЗУСЛОВНО:
|
||
```js
|
||
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
|
||
var menu = window.__wl_menu;
|
||
if (menu && Object.keys(menu).length) { parts.__wl_done = true; wlInject(parts, menu); }
|
||
```
|
||
`Lampa.Api.partNext` — глобальный: через него строят страницу И `main()`
|
||
(`plugin.js:30`), И `category()` (`plugin.js:36`). Никакой проверки активного
|
||
экрана нет → `unshift` кладёт секции в голову пула на КАЖДОМ экране.
|
||
Фильтра по activity в коде НЕ БЫЛО — это не регресс, а исходное отсутствие.
|
||
|
||
2. **Замеры скорости (Kraken, `127.0.0.1:9118`):**
|
||
- `catalog/list?cat=films` прогретый кэш — **4 мс**; первый холодный — **2 250 мс**
|
||
(`hybridCache` модуля Catalog парсит JSON целиком при первом запросе)
|
||
- `watchlist-films.json` (чистая статика) — 1 мс
|
||
- 🔴 `catalog/card?uri=movie/424781` — **66 118 мс (66 сек!)** на 939 байт.
|
||
Это НЕ диск и не фид: `card_parse.initUrl` исполняется как C# и ждёт внутри
|
||
CardController. Отдельный баг, в эту правку не входит.
|
||
- ⚠️ `curl -w %{size_download}` показывает размер **после** распаковки gzip —
|
||
на этом легко обмануться: `watchlist-films.json` = 88 КБ несжатых при 347
|
||
позициях, а `cards.json` = **548 КБ**. Реальный объём фида в разы больше «видимого».
|
||
|
||
### Решение
|
||
|
||
Белый список по активному activity Lampa:
|
||
```js
|
||
function wlOnMain() {
|
||
try {
|
||
var a = Lampa.Activity && Lampa.Activity.active && Lampa.Activity.active();
|
||
var act = (a && a.activity && a.activity.url) ? a.activity.url : '';
|
||
return act === 'main'; // ТОЛЬКО главная
|
||
} catch (e) { return false; }
|
||
}
|
||
```
|
||
Плюс `parts.__wl_done = true` ставить ТОЛЬКО при фактической вставке (иначе после
|
||
возврата из категории на главную секции не доберутся). `console.log` при пропуске —
|
||
для отладки на живом ТВ.
|
||
|
||
### Шаги
|
||
|
||
1. правка `plugin-cub.js` + `build_plugin.py` (INJECT + новый инвариант `wlOnMain`)
|
||
2. `python3 build_plugin.py && node --check plugin-cub.js`
|
||
3. бэкап боевого `catalog/plugin.js` → `scp` нового
|
||
4. **перезапуск Lampa на ТВ обязателен** (`tv_restart.sh` через `luna-send`;
|
||
`docker restart lampac` кэш webOS не сбрасывает)
|
||
5. 🔴 приёмка = **видно в UI**: главная — 4 секции сверху; Фильмы → документальные —
|
||
секций НЕТ. CDP-проба читает `Lampa.Activity.active()` на обоих экранах.
|
||
|
||
### ✅ ИТОГ 2026-09-25: фильтр РАБОТАЕТ, закоммичено
|
||
|
||
- **Коммит `5e55a47`** в репозитории **`~/Developer/lampac-scripts`** (Gitea
|
||
`git_admin/lampac-scripts`), запушен в `origin/main`. Туда переехали
|
||
`scripts/build_plugin.py` + `scripts/plugin-cub.js` — **из `lampac-tmp`, который
|
||
вне git**. `lampac-tmp` больше не источник истины.
|
||
- Приёмка на живом ТВ (CDP): **`act_url = "movie"` (экран Фильмов) → секций Watchlist
|
||
в DOM НЕТ.** Именно то, что просил Alex.
|
||
- Две правки по факту замеров, обе стоили итерации:
|
||
1. **`url` читается с ВЕРХНЕГО уровня** — `active().url`, НЕ `active().activity.url`
|
||
(там его нет вообще: `{component, object, slide, body, is_stopped, is_started}`).
|
||
Первая версия читала `.activity.url` → всегда `undefined` → секции исчезли → «Нету».
|
||
2. **Пустой `url` + пустой `Manifest.catalogs` = инициализация → пропускаем.**
|
||
Иначе строгая проверка отрезала единственный вызов `wlMenu()`, добирающий меню
|
||
после сбоя `/catalog`.
|
||
- 🔴 **Питфолл замеров:** CDP-проба сразу после рестарта Lampa даёт `ready=false`,
|
||
`plugin=false`, `patch=false`, `act_url=ERR` — это НЕ поломка, плагин ещё грузится.
|
||
Мерить надо через ~30 с, иначе легко уйти в ложную диагностику «плагин не подъехал».
|
||
- 🔴 **Задержка секций — архитектурная, в работу ещё НЕ внесена.** Замер: `menu`
|
||
заполнен, а `Manifest.catalogs = NOMANIFEST`. Цепочка: главная CUB рисуется своим
|
||
`cub.main()`, родные секции стартуют сразу; первый `partNext` видит пустой
|
||
`__wl_menu` → уходит в **асинхронный** `wlMenu()` → пока летит запрос, родные секции
|
||
отрисованы, `slice(0, parts_limit)` уже отработал → наши 4 секции попадают в пул
|
||
только в следующем раунде. **План:** тянуть `/catalog` в `startPlugin()`
|
||
**параллельно** с `loadCatalogs()`, чтобы `__wl_menu` лежал в памяти к первой отрисовке.
|
||
|
||
### Питфоллы, найденные при расследовании (не повторять)
|
||
|
||
- `ssh kraken` на каждом вызове печатает `bash: line 1: ifconfig: command not found` —
|
||
это безвредный шум из `~/.bashrc`, НЕ ошибка команды.
|
||
- 🔴 `execute_code` в этой сессии падает на `import` (`No module named 'psutil'`) —
|
||
работаем через `terminal` + `read_file` + `jq`, БЕЗ python.
|
||
- Обрыв `scp` из-за не найденного пути: `wwwroot/catalog.js` НЕ существует (в wwwroot
|
||
лежат только JSON фида) — `plugin.js` живёт в `lampac/catalog/plugin.js` и
|
||
монтируется в `/lampac/module/Catalog/plugin.js`.
|
||
|
||
## 🔴🔴 СЕДЬМАЯ СЕССИЯ: РАБОЧИЙ МЕХАНИЗМ НАЙДЕН — ПЕРЕХВАТ `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/<ID>
|
||
```
|
||
|
||
Клиент на 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=<id>&type=movie`.
|
||
Без `card_parse` Catalog строит URL как `$"{init.host}/{uri}"` → `http://127.0.0.1:9118/movie/424781`
|
||
— такого пути нет, **HTTP 500**, в UI «здесь пусто».
|
||
|
||
**Что даёт `card_parse` (по `CardController.cs`):**
|
||
|
||
| Поле | Механика | Подстановки |
|
||
|---|---|---|
|
||
| `initUrl` | исполняется **C#-выражением** через `CSharpEval`, объект `CatalogInitUrlCard(host, args, uri, query, type)` | ✅ **есть `uri`, `type`, `host`** |
|
||
| `node` | `json.SelectToken(parse.node)` — **чистый JSONPath** | ❌ **нет**, статичен |
|
||
| `name` / `image` / `description` | `ModInit.nodeValue(...)` → `attribute` | — |
|
||
|
||
**Сигнатура роута (критично):**
|
||
```csharp
|
||
[Route("catalog/card")]
|
||
public async Task<ActionResult> Index(string plugin, string uri, string type)
|
||
```
|
||
🔴 **Параметра `cat` НЕТ.** Поэтому `initUrl: $"…/cards-{cat}.json"` даёт `cards-.json` → 404
|
||
→ `html == null` → `BadRequest("html")` → HTTP 500. Карточка **не знает категорию** — она
|
||
получает только `uri` и `type`.
|
||
|
||
**Питфолл JSONPath:** ключ `movie/424781` в `SelectToken` трактуется как **вложенность**
|
||
(`movie` → `424781`) → узел не найден → **HTTP 400 `parse.node`**. Слэш в JSONPath = разделитель.
|
||
|
||
**Итоговое решение (в YAML уже записано, деплой не доделан):**
|
||
```yaml
|
||
card_parse:
|
||
initUrl: |
|
||
return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
|
||
node: card
|
||
name: { attribute: name }
|
||
original_name: { attribute: original }
|
||
year: { attribute: year }
|
||
image: { attribute: poster }
|
||
description: { attribute: overview }
|
||
```
|
||
Файлы `wwwroot/cards/card-<media>-<id>.json` (345 шт., ~1 КБ) с обёрткой `{"card": {...}}`
|
||
и фиксированным путём `$.card` — потому что `node` статичен и один общий файл по `uri`
|
||
не отфильтровать.
|
||
|
||
**Диагностика карточки (воспроизводимо):**
|
||
```bash
|
||
curl -s -o /tmp/c.json -w "HTTP %{http_code}\n" \
|
||
"http://192.168.1.14:9118/catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie"
|
||
# 500 (нет card_parse) / 400 parse.node (node не нашёл узел) / 200 + JObject (ок)
|
||
```
|
||
⚠️ Ошибки карточки **в логи не пишутся** (`docker logs lampac` пуст) — диагностировать только
|
||
по HTTP-коду и телу ответа (`parse.node`, `html`, `json`).
|
||
|
||
## 🔴 ПИТФОЛЛ ДЕПЛОЯ: sudo через ssh без tty
|
||
|
||
`ssh kraken "sudo docker cp …"` падает с `sudo: a terminal is required to read the password`.
|
||
`-t` обязателен, но при этом **нельзя передавать `a && b`** — цепочка рвёт tty-сессию
|
||
(«Connection to kraken closed»). Каждую команду вызывать **отдельно**:
|
||
```python
|
||
full = f"ssh -t {KRAKEN} {cmd}" # одна команда, без &&
|
||
```
|
||
|
||
⚠️ **`docker compose up -d` НЕ пересоздаёт контейнер, если менялся только список volumes.**
|
||
Симптом: `docker inspect` не показывает новое монтирование, патч не виден.
|
||
**Фикс: `docker compose up -d --force-recreate`.** После — проверить
|
||
`docker inspect lampac --format '{{json .Mounts}}' | jq -r '.[].Destination'`.
|
||
|
||
## ✅ ДЕПЛОЙ: что залито и чем (2026-09-22, третья сессия)
|
||
|
||
**Скрипты (на Mac, `/Users/admin/lampac-tmp/`):**
|
||
|
||
| Файл | Роль |
|
||
|---|---|
|
||
| `gen_watchlist_v2.py` | генератор: `.md` → постеры/TMDB-ID/карточки (Radarr → TMDB → Кинопоиск-фолбэк) |
|
||
| `patch_plugin.py` | патч `plugin.js`: `+enumerable:true,configurable:true` в оба `defineProperty` |
|
||
| `plugin-patched.js` | результат патча (10738 Б), `node --check` чист |
|
||
| `deploy_lampac.py` | заливка JSON/YAML/plugin.js + volume + пересоздание + самопроверка |
|
||
| `watchlist.yaml` | описание каталога для Catalog (4 категории + `card_parse`) |
|
||
| `docker-compose.yml` | + volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` |
|
||
| `movies-watchlist.md` | локальная копия списка (682 строки) для прогонов |
|
||
|
||
**Кэши генератора (в `$HOME`):**
|
||
- `~/.lampac_tmdb_cache.json` — `{title|year: [tmdb_id, media_type, poster_path]}` (поиск)
|
||
- `~/.lampac_tmdb_details.json` — `{movie/ID: {overview, genres, cast, runtime, vote, …}}` (детали карточки)
|
||
|
||
**Что даёт прогон (замер):**
|
||
```
|
||
films: 360 постеров 308 TMDB-ID 312
|
||
new: 33 постеров 30 TMDB-ID 30
|
||
cartoons 3 постеров 3 TMDB-ID 3
|
||
anime: 7 постеров 7 TMDB-ID 7
|
||
источники: Radarr 9, TMDB 343, Кинопоиск-фолбэк 51, карточек 352
|
||
```
|
||
|
||
**Артефакты в `wwwroot/` (все на HDD Кра́кена):**
|
||
`watchlist.json` · `watchlist-{films,new,cartoons,anime}.json` · `cards.json` · `cards/card-<key>.json`
|
||
|
||
**Команды (порядок):**
|
||
```bash
|
||
cd /Users/admin/lampac-tmp
|
||
python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
|
||
python3 deploy_lampac.py /tmp/wl-out
|
||
```
|
||
⚠️ После правки генератора **перезапускать его**, а не только деплой — иначе в `wwwroot`
|
||
уедет старый формат файлов (один раз на этом потерял `cards.json` как список вместо словаря).
|
||
|
||
## 🔴 АРХИТЕКТУРНАЯ ОШИБКА 2026-09-22 — почему каталог «не работает»
|
||
|
||
**Что хотел Alex (дословно):**
|
||
|
||
> «Я предполагал что будет отображаться то что и раньше (список cub) но В НЕГО будет
|
||
> сверху добавлено несколько секций в которых будут РЕАЛЬНЫЕ ЭЛЕМЕНТЫ — а не сгенерируют
|
||
> пустые заглушки! Нахуя это надо то?! Чё блядь значит оно так и должно быть?!»
|
||
|
||
**Что сделано вместо этого — две независимые ошибки:**
|
||
|
||
> ⚠️ **ОПРОВЕРГНУТО ТЕСТОМ 2026-09-22 (поздний вечер).** Раздел ниже описывает
|
||
> **неверную гипотезу** («`menu` подменяет / `main` добавляет»). Проверка на живом
|
||
> сервере показала: **блока `main` в YAML не существует** — он ломает парсинг.
|
||
> Подмену разделов делает **`plugin.js`**, а не YAML. Читать раздел
|
||
> **«🔴 ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН»** ниже — там правильная механика.
|
||
|
||
### Ошибка 2 — источник данных: парсинг `.md` вместо Radarr API
|
||
|
||
Генератор читал `movies-watchlist.md` — **текстовый список названий**. В нём
|
||
**нет ни TMDB ID, ни постеров**. Результат:
|
||
|
||
| Метрика | Значение |
|
||
|---|---|
|
||
| Всего позиций (films) | 360 |
|
||
| Из них с постером (`img != null`) | **0** |
|
||
| Из них с реальным ID (`film/...`) | **18** |
|
||
|
||
Остальные 342 отдавались с `id` = **ссылка на поиск Кинопоиска**
|
||
(`index.php?kp_query=...`). Отсюда жалоба «при открытии элемента пишет *здесь пусто*»:
|
||
Lampa идёт по ссылке, получает страницу поиска, а не фильм.
|
||
|
||
**Замер 2026-09-22 (доказательство, воспроизводимо):**
|
||
|
||
```bash
|
||
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films" \
|
||
| jq '[.results[] | select(.img == null)] | length' # → 360
|
||
curl -s "..." | jq '.results | length' # → 360
|
||
curl -s "..." | jq '[.results[] | select(.id | startswith("film/"))] | length' # → 18
|
||
```
|
||
|
||
То есть **постеров 0 из 360**, рабочих карточек 18 из 360.
|
||
|
||
**Почему «девять» с маленькой буквы и `Gods don't give gifts`** — это не баг каталога,
|
||
а буквально строки файла (проверено `grep -n`):
|
||
|
||
```
|
||
56: - [ ] Gods don't give gifts
|
||
64: - [ ] ❓ девять
|
||
```
|
||
|
||
Каталог отдаёт строки как есть. Обе — в секции `## Новинки` (отступ 2 пробела, подсписок).
|
||
Alex: «Отображаются элементы типа "девять"(с маленькой буквы), Gods don't give gifts —
|
||
который вообще ещё не вышел — короче не реальные фильмы а буквально пункты списка.»
|
||
|
||
**Правильный источник — Radarr API.** Alex: «Они уже резолвятся же блядь!! Оно же
|
||
добавлялось в arr!» — TMDB ID и постеры уже есть там, потому что фильмы добавлялись
|
||
в Radarr.
|
||
|
||
```bash
|
||
# API-ключ Radarr
|
||
grep -oE "<ApiKey>[^<]+</ApiKey>" /srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config.xml
|
||
# → cbcb8ec320104359920c0f611d503c7b
|
||
|
||
# ⚠️ ВАЖНО: -L обязателен! Без него Radarr отдаёт 307, тело пустое
|
||
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length
|
||
# → 50
|
||
```
|
||
|
||
**Форма ответа Radarr (то, что нужно было брать с самого начала):**
|
||
|
||
```json
|
||
{
|
||
"tmdbId": 1292695,
|
||
"title": "They Will Kill You",
|
||
"year": 2026,
|
||
"hasFile": true,
|
||
"images": [{"coverType":"poster",
|
||
"remoteUrl":"https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28R"}]
|
||
}
|
||
```
|
||
|
||
Есть **всё**: TMDB ID → рабочий `href`, `remoteUrl` → постер, `title`/`year` → название,
|
||
`hasFile` → скачан или нет. Плюс постеры локально: `/config/MediaCover/` (46 каталогов).
|
||
|
||
⚠️ **Покрытие:** в Radarr **50 фильмов**, в `.md` — **360**. Разница (310) — то, что
|
||
в список записано, но в Radarr не заказано. Что с ними делать — **решение Alex
|
||
не получено**, открытый вопрос №1 ниже.
|
||
|
||
### Что НЕ надо было делать (записано, чтобы не повторять)
|
||
|
||
- Парсить текст вместо API, где данные уже резолвлены
|
||
- Складывать `menu` + `main` одновременно
|
||
- Считать «список названий» готовым каталогом для UI с постерами
|
||
|
||
### Alex: корневая претензия к процессу
|
||
|
||
> «А нахуя ты это так реализовал?? Подумать блядь вообще не твое!»
|
||
|
||
Разбор: задача звучала как «покажи список в UI» — и была прочитана буквально
|
||
(«отдать строки файла»). **Надо было сначала выяснить, откуда брать постеры и ID**
|
||
(а ответ — Radarr, куда фильмы и добавлялись), а не начинать с парсера текста.
|
||
|
||
## Задача
|
||
|
||
Показывать список фильмов Alex (`family/documents/movies-watchlist.md`) **в UI Lampa на ТВ** — каталогом.
|
||
Не локальный плейбэк. Формулировка Alex: «отображение фильмов из списка».
|
||
|
||
**Уточнение от 2026-09-22 (важное!):** каталог **добавляется СЕКЦИЯМИ СВЕРХУ** к родному
|
||
каталогу Lampa. Родной CUB-контент **не подменяется и не исчезает**. Элементы — **реальные**
|
||
фильмы с постерами и карточками, а не пустые заглушки (см. «🔴 АРХИТЕКТУРНАЯ ОШИБКА»).
|
||
|
||
⚠️ **Что НЕ спрашивать повторно.** Задача — **отображение**, не воспроизведение.
|
||
Список — это перечень, а не источник видео. Вопрос «а что играть по клику?» —
|
||
**выдумка сверх постановки**, Alex на него реагировал раздражением («я чё тебе сказал в постановке задачи»).
|
||
Штатное воспроизведение Lampa (торренты/онлайн) работает само, отдельно решать нечего.
|
||
|
||
## ✅ Затык снят: чем это делается
|
||
|
||
В образе Lampac уже есть модуль **`Catalog`** — YAML-каталоги → плагин Lampa `/catalog.js`.
|
||
Сейчас **выключен**. Писать плагин с нуля **не нужно**.
|
||
|
||
## Как работает Catalog (разобрано по коду 2026-09-22)
|
||
|
||
**Файлы в контейнере:** `/lampac/module/Catalog/` — `Controllers/`, `plugin.js`, `README.md`,
|
||
`sites/*.yaml` (10 готовых примеров: `vokino`, `rezka`, `kinopub`, `anime`, …).
|
||
|
||
### Механика
|
||
|
||
| Что | Как |
|
||
|---|---|
|
||
| Данные | `sites/<site>.yaml` — описание сайта: `host`, `menu`, `search`, `list`, `content` |
|
||
| Подстановки | `override/<site>.yaml` и `override/_.yaml` — переопределения поверх базового |
|
||
| Роуты | `/catalog.js` (плагин), `/catalog` (JSON каналов), `/catalog/list`, `/catalog/card` |
|
||
| WAF | `^/catalog/` → **10 запросов/сек** |
|
||
| Имя файла | только `[a-z0-9-]`; есть fallback-поиск по `displayname` |
|
||
|
||
### Ключевое: обязателен HTTP
|
||
|
||
```csharp
|
||
if (!url.StartsWith("http"))
|
||
url = $"{init.host}/{url}"; // ListController.cs:87
|
||
```
|
||
`host` из YAML сканируется **по HTTP** (`Http.Get` / Playwright / RCH).
|
||
**Локальные файлы напрямую Catalog читать не умеет** — только HTTP-ответ.
|
||
|
||
### Два режима парсинга
|
||
|
||
```csharp
|
||
bool? jsonPath = contentParse.jsonPath ?? init.jsonPath;
|
||
|
||
if (jsonPath == true) json = JToken.Parse(html); // JSON-режим
|
||
else doc.LoadHtml(html); // HTML-режим
|
||
```
|
||
|
||
- `jsonPath: true` → ответ парсится как **JSON**, элементы берутся по `nodes` (`$.channels[*].details`)
|
||
- иначе → HTML через HtmlAgilityPack
|
||
|
||
🔴 **Это и есть рычаг:** любой **свой HTTP-эндпоинт, отдающий JSON нужной формы**, подойдёт как `host`.
|
||
Тогда задача сводится к генерации JSON + YAML-обёртки, без правки кода Lampac.
|
||
|
||
### Форма элемента (`content` в YAML)
|
||
|
||
```yaml
|
||
content:
|
||
nodes: "$.channels[*].details" # jsonPath-выражение (JSON) или CSS/XPath (HTML)
|
||
name: { attribute: name }
|
||
original_name:{ attribute: originalname }
|
||
href: { attribute: ident }
|
||
image: { attribute: poster }
|
||
year: { attribute: released }
|
||
serial_key: { attribute: type, format: | return value is "serial" ? value : null; }
|
||
args:
|
||
- { name_arg: vote_average, attribute: rating_kp }
|
||
```
|
||
|
||
Карточка — отдельная секция `card_parse` + `card_args` (описание, рейтинги, жанры, страна).
|
||
Полный живой пример — `sites/vokino.yaml` (копия в [[personal/tech/lampac-deployment]], раздел Catalog).
|
||
|
||
## Включение модуля — 2 правки
|
||
|
||
```jsonc
|
||
// current.conf (внутри контейнера /lampac/current.conf)
|
||
"SkipModules": [ "Tracks", "Transcoding", ... ] // ← убрать "Catalog" (строка ~277)
|
||
"plugins": { ..., "catalog": true } // ← было false (строка ~1653)
|
||
```
|
||
|
||
🔴 **ПИТФОЛЛ — `init.conf` НЕ смонтирован.** В нашем `docker-compose.yml` нет volume на `/lampac/init.conf`,
|
||
файл живёт в образе. Правки через `docker exec` **исчезнут при пересоздании контейнера**
|
||
(`docker compose up`, watchtower-обновление образа). Варианты:
|
||
- (а) добавить volume `./config/init.conf:/lampac/init.conf:ro` — надёжно, но нужен первичный `docker cp`
|
||
- (б) править через **AdminPanel** `/adminpanel` (модуль, правит `init.conf` + `current.conf`)
|
||
- (в) `docker cp` + commit — плохо
|
||
|
||
**Рекомендация: (а)** — конфиг на HDD, переживает пересоздание.
|
||
|
||
## Архитектура решения
|
||
|
||
```
|
||
family/documents/movies-watchlist.md ← источник (vault, git-синхрон)
|
||
│ существующий парсер: watchlist-sync/parser.py
|
||
▼
|
||
генератор (Python, на Кра́кене)
|
||
│ пишет JSON нужной формы
|
||
▼
|
||
/lampac/... JSON-эндпоинт, доступный по HTTP с Lampac
|
||
│
|
||
▼
|
||
sites/watchlist.yaml ← Catalog-описание, host=<наш JSON URL>
|
||
menu: Фильмы / Мультфильмы / Аниме (по секциям .md)
|
||
│
|
||
▼
|
||
Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js
|
||
```
|
||
|
||
**Откуда отдавать JSON — варианты (выбрать на этапе реализации):**
|
||
|
||
| # | Где | Плюс | Минус |
|
||
|---|---|---|---|
|
||
| 1 | **статика в `/lampac/wwwroot/`** | ничего не поднимать, Lampac сам отдаёт | только чтение, обновление = перезапись файла |
|
||
| 2 | **отдельный лёгкий HTTP-сервер** на Кра́кене | есть API/логика | ещё один контейнер |
|
||
| 3 | `file://` | — | ❌ не поддерживается (только http) |
|
||
|
||
**Склоняюсь к (1)** — генерировать `wwwroot/watchlist.json`, Lampac отдаёт его сам, без новых сущностей.
|
||
|
||
## Обновление каталога — реализовано
|
||
|
||
Скрипт `~/scripts/watchlist-catalog.sh` (root:root, ставить только `sudo install -m 755`):
|
||
|
||
1. `python3 $LAMPAC/scripts/gen_watchlist.py <watchlist.md> $LAMPAC/wwwroot/watchlist.json`
|
||
2. `docker restart lampac` — **обязательно**, сбрасывает кэш Catalog (см. питфолл 3)
|
||
|
||
Cron: `30 2 * * *` → лог `~/logs/watchlist-catalog.log`.
|
||
Время выбрано после `watchlist-nightly` (01:00) и `watchlist-cleanup` (02:00).
|
||
|
||
Бэкап crontab перед установкой: `~/crontab-backup-20260922-112915.txt`.
|
||
|
||
⚠️ **`watchlist-catalog.sh` сделан отдельной задачей, а не врезан в `watchlist-nightly.sh`.**
|
||
Причина: `nightly` — рабочий боевой скрипт, его падение остановило бы весь пайплайн списка.
|
||
Отдельный cron падает изолированно.
|
||
|
||
## Что осталось — ПЕРЕДЕЛКА (после признания архитектуры неверной)
|
||
|
||
| # | Шаг | Статус |
|
||
|---|---|---|
|
||
| 1 | Генератор берёт данные **из Radarr API** (`localhost:7878/api/v3/movie`), а не парсит текст | ✅ сделано (9 из Radarr) |
|
||
| 2 | Подставлять в `id` → `tmdbId`, в `img` → `images[].remoteUrl` (постер), `year` → `year` | ⏳ |
|
||
| 3 | ~~Из `sites/watchlist.yaml` убрать блок `menu`, оставить `main`~~ ❌ **НЕВОЗМОЖНО** — `main` в YAML ломает парсинг, `menu` обязателен. Подмену делает `plugin.js`. См. «🔴 ПИТФОЛЛ YAML» | ❌ опровергнуто |
|
||
| 4 | Постеры кэшировать локально (Radarr `MediaCover/` или проксировать через `tmdbproxy`) | ⏳ |
|
||
| 5 | Фикс `plugin.js` (`enumerable:true`) — **отдельная независимая правка**, см. ниже | ⏳ ждёт апрува |
|
||
|
||
**Порядок:** шаги 1–4 закрывают жалобу «пустые заглушки + исчез CUB».
|
||
Шаг 5 — про то, что источника вообще не видно в списке выбора.
|
||
|
||
Приёмка (формулировка Alex): **родной список CUB на месте, сверху добавлены ряды
|
||
с реальными фильмами из списка, у элементов есть постеры и рабочие карточки.**
|
||
|
||
## Диагностический скрипт для ТВ (`/tmp/lampa_probe.py`)
|
||
|
||
Правки в UI Lampa по SSH напрямую не сделать, но **состояние читается** через DevTools:
|
||
|
||
```python
|
||
pages = json.load(urllib.request.urlopen("http://192.168.1.70:9998/json", timeout=8))
|
||
target = next(p for p in pages if "com.lampac.tv" in p.get("url",""))
|
||
ws = websocket.create_connection(target["webSocketDebuggerUrl"], timeout=10)
|
||
# Runtime.evaluate:
|
||
# Lampa.Storage.field('source')
|
||
# Object.keys(Lampa.Api.sources).join('|')
|
||
# Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список')
|
||
# Lampa.Api.sources['Мой список'].getCatalog()
|
||
```
|
||
|
||
Требует `pip3 install websocket-client` (стоит). См. [[family/how-to/tv-lampac-plugin]].
|
||
|
||
## Известные ограничения
|
||
|
||
| Ограничение | Детали |
|
||
|---|---|
|
||
| **Постеров нет** | `image` пустой. Опционально — тянуть через TMDB-кэш Lampac (`tmdbproxy`) |
|
||
| **Карточки нет** | Только список названий. `card_parse` не настроен — Lampa может показывать пустую карточку |
|
||
| **`🗓️` upcoming показываются** | Не прячем. Решение Alex не запрашивалось |
|
||
| **Перезапуск контейнера** | Каждое обновление каталога = `docker restart lampac` (~30 сек простоя). Кэш не инвалидируется иначе |
|
||
|
||
## ⚠️ Что проверено, а что нет
|
||
|
||
**Проверено по HTTP (серверная часть):**
|
||
|
||
| Проверка | Результат |
|
||
|---|---|
|
||
| `/catalog.js` по IP | 200, ~32 КБ, `api_host:'http://192.168.1.14:9118'` |
|
||
| Каталог в `/catalog` | **7 ключей**, «Мой список» первым (Rezka, Kinobase, FilmixApp, VoKino, Eneyida, KinoUkr, Мой список) |
|
||
| JSON-файлы | `watchlist.json` + `watchlist-{films,new,cartoons,anime}.json` — все 200 |
|
||
| Выдача по категориям | films 360 · new **33** · cartoons 3 · anime 7 (после фикса сирот 2026-09-22) |
|
||
| Формат ответа `/catalog/list` | `{page, results, total_pages}`, `total_pages: 1` для films |
|
||
| Сортировка | ⭐ → рейтинг ↓ → ужасы |
|
||
| Чистота названий | эмодзи-маркеров 0 |
|
||
| Сирота «Конец света» | ❌ отсутствует во всех 4 категориях |
|
||
|
||
**НЕ проверено:**
|
||
|
||
- ✅ **UI Lampa на ТВ — ПРИЧИНА НАЙДЕНА 2026-09-22** (см. ниже «🔴 ГЛАВНЫЙ ПИТФОЛЛ»).
|
||
Диагностика через Chrome DevTools ТВ (`http://192.168.1.70:9998/json`):
|
||
- ТВ **физически достаёт** Кра́кен — `curl` с ТВ отдаёт `/catalog` 200 за 0.26 с.
|
||
- Lampa **работает и ходит на сервер** — соединение `192.168.1.70 → 192.168.1.14:9118` ESTABLISHED.
|
||
- `Storage.field('source')` = **`'Мой список'`** — источник уже выбран!
|
||
- `Lampa.Api.sources['Мой список']` **существует** и отдаёт каталог
|
||
(`getCatalog()` → `search, search_lazy, movie, cartoons, anime, menu, main`).
|
||
- ❌ НО: `Object.keys(Lampa.Api.sources)` = `'tmdb|cub'` — **источника нет в перечислении**.
|
||
- Причина: `defineProperty(..., {get: ..., enumerable:false, configurable:false})`.
|
||
- **Диагноз: баг в `plugin.js` самого Lampac, а не в нашей настройке.** UI Lampa строит
|
||
список источников через `Object.keys()` — «Мой список» из него выпадает, поэтому в UI
|
||
источник не видно и выбрать его нельзя.
|
||
- ❌ Боевой прогон крона в 02:30 (скрипт отработал вручную успешно).
|
||
- ❌ Карточка/постеры в UI.
|
||
|
||
## 🔴 ГЛАВНЫЙ ПИТФОЛЛ: источник не виден в UI Lampa (`enumerable: false`)
|
||
|
||
**Симптом.** Alex: «Ни на главной ни в Фильмах нет! Там только Сейчас смотрят, рекомендуем итд».
|
||
Плагин добавлен, серверная часть отдаёт всё корректно, а в UI источника нет.
|
||
|
||
**Диагностика (по возрастанию точности):**
|
||
|
||
1. `ssh kraken 'sudo ss -tn | grep ":9118"'` — сначала показал **0 соединений** (ложный вывод:
|
||
замер сделан в момент старта Lampa). Через 30 с на ТВ:
|
||
`netstat -tn | grep 9118` → **1 ESTABLISHED + 6 TIME_WAIT** — Lampa скачала все файлы.
|
||
2. Foreground-приложение: `luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}`
|
||
→ `com.lampac.tv`. Запускать надо `launch` с **экранированным JSON в кавычках**,
|
||
иначе `errorCode: 2 invalid parameters`.
|
||
3. Доступ к отладчику ТВ: **`curl http://192.168.1.70:9998/json`** — webOS отдаёт Chrome
|
||
DevTools Protocol на порту 9998 (у `WebAppMgr` в аргументах `--remote-debugging-port=9998`).
|
||
Через `websocket` + `Runtime.evaluate` читаются переменные живой Lampa.
|
||
4. Ключевой замер: `JSON.stringify(Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список'))`
|
||
→ `{"enumerable":false,"configurable":false}`.
|
||
|
||
**Корень.** В `/lampac/module/Catalog/plugin.js` (строка 59, `loadCatalogs()`, **два вхождения**):
|
||
|
||
```js
|
||
Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; } });
|
||
```
|
||
|
||
Свойство создаётся не-enumerable → `Object.keys()` его не видит → Lampa не рисует источник
|
||
в панели выбора. При этом `Api.sources['Мой список']` доступен напрямую и каталог отдаёт.
|
||
|
||
**Фикс** (одна правка, два вхождения):
|
||
|
||
```js
|
||
Object.defineProperty(Lampa.Api.sources, key,
|
||
{ get: function(){ return Api; }, enumerable: true, configurable: true });
|
||
```
|
||
|
||
**Как применять — `plugin.js` живёт в образе, не на HDD!** (проверено:
|
||
`docker inspect` не показывает монтирования на `module/Catalog`, монтируются только
|
||
`catalog/sites` и `catalog/override`). Правка через `docker exec` **исчезнет при обновлении образа**.
|
||
|
||
Предложенный путь (на апруве у Alex, 2026-09-22):
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
volumes:
|
||
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
|
||
```
|
||
|
||
```bash
|
||
# 1. забрать, поправить локально (sed/grep по файлу не применять — только patch)
|
||
ssh kraken 'docker exec lampac cat /lampac/module/Catalog/plugin.js' > /tmp/catalog-plugin.js
|
||
# 2. бэкап
|
||
cp /tmp/catalog-plugin.js /tmp/catalog-plugin.js.bak-$(date +%Y%m%d-%H%M%S)
|
||
# 3. правка двух вхождений defineProperty → +enumerable:true,configurable:true
|
||
# 4. проверка синтаксиса ПЕРЕД заливкой
|
||
node --check /tmp/catalog-plugin.js
|
||
# 5. на HDD, добавить volume, пересоздать
|
||
scp /tmp/catalog-plugin.js kraken:/srv/.../lampac/catalog/plugin.js
|
||
ssh kraken 'cd /srv/.../lampac && docker compose up -d'
|
||
# 6. проверка на ТВ: Object.keys(Lampa.Api.sources) должен стать 'tmdb|cub|Мой список'
|
||
```
|
||
|
||
**Откат:** убрать строку volume из compose → `docker compose up -d`. Плюс бэкап файла в `/tmp`.
|
||
|
||
**Статус: правка НЕ применена** — ждёт апрува Alex (2026-09-22).
|
||
|
||
## Готовые команды диагностики ТВ (переиспользовать)
|
||
|
||
```bash
|
||
# 1. Что запущено на ТВ (luna-send пишет в tty → нужен script -q -c)
|
||
ssh root@192.168.1.70 'script -q -c "luna-send -n 1 -f \
|
||
luna://com.webos.applicationManager/getForegroundAppInfo {}" /dev/null'
|
||
|
||
# 2. Запустить приложение (JSON экранирован И обёрнут в кавычки)
|
||
ssh root@192.168.1.70 'script -q -c "luna-send -n 1 -f \
|
||
luna://com.webos.applicationManager/launch \"{\\\"id\\\":\\\"com.lampac.tv\\\"}\"" /dev/null'
|
||
|
||
# 3. Достаёт ли ТВ Кра́кен
|
||
ssh root@192.168.1.70 'curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
|
||
--max-time 8 http://192.168.1.14:9118/catalog'
|
||
|
||
# 4. Живые соединения с Lampac (на ТВ, не на сервере!)
|
||
ssh root@192.168.1.70 'netstat -tn 2>/dev/null | grep 9118'
|
||
|
||
# 5. DevTools-цель Lampa на ТВ
|
||
curl -s --max-time 8 "http://192.168.1.70:9998/json" | /usr/bin/jq -r '.[] | "\(.type): \(.url)"'
|
||
```
|
||
|
||
Скрипты-пробы DevTools: `/tmp/lampa_probe.py` (состояние источника),
|
||
`/tmp/lampa_probe3.py` (асинхронный вызов `menu()` каталога).
|
||
|
||
⚠️ `tcpdump` на Кра́кене **нет** — для съёма трафика ставить не стали, использован
|
||
DevTools-подход (дал больше: видно состояние JS, а не только пакеты).
|
||
|
||
⚠️ `luna://com.webos.service.capture/takeScreenShot` на этом ТВ **не существует**
|
||
(`Unknown method`) — скриншот так не снять.
|
||
|
||
## Файлы реализации (на HDD Кра́кена)
|
||
|
||
Корень: `/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/`
|
||
|
||
| Путь | Роль |
|
||
|---|---|
|
||
| `config-module/init.conf` | включает Catalog → `/lampac/init.conf:ro` |
|
||
| `catalog/sites/watchlist.yaml` | описание каталога |
|
||
| `catalog/override/` | переопределения (пусто) |
|
||
| `wwwroot/watchlist.json` | все секции |
|
||
| `wwwroot/watchlist-<code>.json` | по файлу на категорию |
|
||
| `scripts/gen_watchlist.py` | генератор |
|
||
| `docker-compose.yml` | 7 монтирований; бэкапы `.bak-*` |
|
||
|
||
На хосте: `~/scripts/watchlist-catalog.sh`, лог `~/logs/watchlist-catalog.log`.
|
||
|
||
## Откат
|
||
|
||
```bash
|
||
# выключить каталог
|
||
ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \
|
||
/srv/.../lampac/config-module/init.conf && docker restart lampac'
|
||
# в Lampa убрать плагин catalog.js
|
||
# cron: crontab -l | grep -v watchlist-catalog | crontab -
|
||
```
|
||
|
||
Ничего в коде Lampac не патчилось — только конфиг и данные.
|
||
|
||
## Статус: ✅ РЕАЛИЗОВАНО 2026-09-22
|
||
|
||
Каталог «Мой список» работает, отдаётся в Lampa через `/catalog.js`.
|
||
|
||
### Что сделано
|
||
|
||
| # | Шаг | Результат |
|
||
|---|---|---|
|
||
| 1 | Включён модуль Catalog | `SkipModules` без Catalog + `catalog: true` → `/catalog.js` 200 |
|
||
| 2 | `init.conf` смонтирован | `./config-module/init.conf:/lampac/init.conf:ro` — правится на HDD, не теряется |
|
||
| 3 | `sites/` + `override/` смонтированы | `./catalog/sites`, `./catalog/override` |
|
||
| 4 | `wwwroot` смонтирован | `./wwwroot` — Lampac сам отдаёт JSON по HTTP |
|
||
| 5 | Генератор написан | `lampac/scripts/gen_watchlist.py` |
|
||
| 6 | `sites/watchlist.yaml` создан | `lampac/catalog/sites/watchlist.yaml` |
|
||
| 7 | Автообновление в cron | `30 2 * * *` → `~/scripts/watchlist-catalog.sh` |
|
||
| 8 | Проверка | 4 категории: films 360, new 34, cartoons 3, anime 7 |
|
||
|
||
### Архитектура (финальная)
|
||
|
||
```
|
||
family/documents/movies-watchlist.md ← источник
|
||
│ gen_watchlist.py (свой парсер, ~200 строк)
|
||
▼
|
||
lampac/wwwroot/watchlist.json ← все секции
|
||
lampac/wwwroot/watchlist-<code>.json ← по файлу на категорию
|
||
│ Lampac отдаёт wwwroot по HTTP сам
|
||
▼
|
||
lampac/catalog/sites/watchlist.yaml ← описание для Catalog
|
||
│ plugin=watchlist, jsonPath: true, nodes: $.items[*]
|
||
▼
|
||
Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js
|
||
```
|
||
|
||
### 🔴 Питфоллы, найденные по ходу (не повторять)
|
||
|
||
1. **Catalog ВЫБРАСЫВАЕТ элемент с пустым `href`.**
|
||
`ListController.cs`: `if (!IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href))`.
|
||
Симптом: из 404 позиций отдалось 52 — ровно те, у кого есть ссылка Кинопоиска.
|
||
**Фикс:** генератор подставляет fallback-href
|
||
`https://www.kinopoisk.ru/index.php?kp_query=<название>`.
|
||
|
||
2. **Catalog НЕ подставляет `{cat}` в `nodes`.**
|
||
JSONPath статичен, отфильтровать секцию внутри одного файла нечем.
|
||
Симптом: все категории отдавали весь список (404 вместо 360/34/3/7).
|
||
**Фикс:** генератор пишет отдельный плоский файл на категорию
|
||
(`watchlist-films.json` и т.д.), `nodes: "$.items[*]"` одинаков для всех.
|
||
|
||
3. **Кэш Catalog без версии данных.**
|
||
Ключ `catalog:{plugin}:{search}:{sort}:{cat}:{page}` — не зависит от содержимого.
|
||
Симптом: после перегенерации JSON выдача не менялась.
|
||
**Фикс:** `docker restart lampac` в скрипте обновления.
|
||
|
||
4. **`init.conf` не был смонтирован.**
|
||
Файл живёт в образе; правки через `docker exec` теряются при пересоздании.
|
||
Правки шли в `current.conf` (сгенерированный) — неверная цель.
|
||
**Фикс:** volume `:ro` + свой минимальный `init.conf` (перекрывает `base.conf`).
|
||
|
||
5. **`api_host` подставляется из запроса** (`{localhost}` → хост, по которому пришли).
|
||
По IP `192.168.1.14` отдаёт правильный адрес — специально ничего настраивать не нужно.
|
||
|
||
6. **Кириллица в inline-jq внутри `ssh '...'`** ломает команду (tirith + quoting).
|
||
Выносить в файл или убирать строки с кириллицей.
|
||
|
||
7. **Осиротевшие строки в `## Новинки` давали дубли в каталоге.**
|
||
Строки без `👍`/`👎` (например `- [ ] ⬇️ Название`) `process-thumbs` не трогал —
|
||
они висели в Новинках навсегда. Если фильм уже `[x]` в главном списке — получался дубль:
|
||
непросмотренный «Конец света» при просмотренном оригинале.
|
||
**Фикс в `watchlist-sync/parser.py`** (удаление сирот + флаг `_patch_dropped`) и `cli.py`.
|
||
Разбор: [[family/projects/watchlist-automation]], секция «ОСИРОТЕВШИЕ СТРОКИ».
|
||
|
||
8. **Двойное экранирование при правке `parser.py` скриптами.**
|
||
`r'\1'` превращался в байт `\x01`, `r'\b'` — в `\x08`. Синтаксис валиден, регекспы молчат.
|
||
**Проверять файл на `chr(1)` и `chr(8)` после патча.**
|
||
|
||
### Управление
|
||
|
||
```bash
|
||
ssh kraken
|
||
cd /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac
|
||
|
||
# перегенерировать каталог + сбросить кэш
|
||
bash ~/scripts/watchlist-catalog.sh
|
||
|
||
# логи
|
||
tail -f ~/logs/watchlist-catalog.log
|
||
|
||
# только генератор (без перезапуска)
|
||
python3 scripts/gen_watchlist.py \
|
||
/home/kraken/obsidian/family/documents/movies-watchlist.md \
|
||
wwwroot/watchlist.json
|
||
```
|
||
|
||
### Откат
|
||
|
||
```bash
|
||
# выключить каталог
|
||
ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \
|
||
/srv/.../lampac/config-module/init.conf && docker restart lampac'
|
||
# в Lampa убрать плагин catalog.js
|
||
```
|
||
|
||
## Правила отбора и сортировки (утверждено Alex 2026-09-22)
|
||
### Фильтрация
|
||
|
||
| Правило | Действие |
|
||
|---|---|
|
||
| `[ ]` непросмотренные | **показывать** |
|
||
| `[x]` просмотренные | ❌ скрыть |
|
||
| `👎` | ❌ скрыть |
|
||
| `🗓️` upcoming | показывать (решить при реализации — есть в файле) |
|
||
| `❓` ambiguous | показывать (название как есть) |
|
||
|
||
### Сортировка внутри списка — трёхключевая
|
||
|
||
1. **`⭐` (U+2B50) — первыми**
|
||
2. **Рейтинг — по убыванию** (IMDb/KP из строки файла; строки `[название](kp) (год) IMDb N.N KP N.N`)
|
||
3. **Ужасы (`🧟♀️`, TMDB genre_id=27) — последними**
|
||
|
||
⚠️ **Только `⭐` (U+2B50)** — как в `sync-down` (см. [[family/projects/watchlist-automation]]).
|
||
`✭` (U+272D) приоритетом **не является**.
|
||
|
||
### Порядок секций
|
||
|
||
1. **Главный список — первым.** Это `## Фильмы`
|
||
2. Дальше — **остальные по порядку в файле**: `## Новинки - 👍/👎`, `## Мультфильмы`, `## Аниме`, …
|
||
|
||
⚠️ `## Новинки👎` — **не показывать** (это скрытая секция отказов).
|
||
⚠️ Секция `## Новинки` содержит `👍`/`👎`; `👎`-строки прячем, `👍` показываем.
|
||
|
||
### Источник рейтинга
|
||
|
||
В строках со ссылками Кинопоиска рейтинг **уже в тексте**: `IMDb 7.6 KP 8.2`.
|
||
Для строк без рейтинга (например `- [ ] Gods don't give gifts`) — сортировать как `0`, в конец группы.
|
||
Опционально: добирать через TMDB (кэш `tmdbproxy` в Lampac) — решить при реализации.
|
||
|
||
## 🟢 РЕШЕНИЕ: первая категория игнорирует `format` (2026-09-22, вторая сессия)
|
||
|
||
**Это была настоящая причина «пустого каталога».** Не `main`, не кэш, не монтирование.
|
||
|
||
### Симптом
|
||
|
||
`cat=films` отдавал **360 элементов** с `img: null` и `id` = `index.php?kp_query=...`
|
||
(ссылка на поиск Кинопоиска). При этом `watchlist-films.json` на диске содержал
|
||
совсем другое. Клик по элементу → Lampa идёт на страницу поиска → «здесь пусто».
|
||
|
||
### Диагностика (воспроизводимая)
|
||
|
||
Подменил `watchlist-films.json` пробником из 1 элемента («Project Hail Mary»
|
||
с реальным TMDB ID и постером). Результат запросов:
|
||
|
||
| Запрос | Ответ | Что это значит |
|
||
|---|---|---|
|
||
| `cat=films` | **360** (старые данные) | файл НЕ прочитан |
|
||
| `cat=films&sort=rating` | **1** ✅ | файл прочитан |
|
||
| `cat=new` | 33 ✅ | файл прочитан |
|
||
| `cat=cartoons` | 3 ✅ | файл прочитан |
|
||
| `cat=anime` | 7 ✅ | файл прочитан |
|
||
|
||
Пробник был 343 байта, HTTP внутри контейнера отдавал 343 байта — **но Catalog
|
||
возвращал 360**. Значит Catalog читал **другой URL**.
|
||
|
||
### Корень — `ListController.cs:50–87`
|
||
|
||
```csharp
|
||
string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
|
||
? init.list?.firstpage : init.list?.uri)}"; // ← строка 50
|
||
...
|
||
else if (!string.IsNullOrEmpty(cat))
|
||
{
|
||
var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat));
|
||
string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-");
|
||
if (!string.IsNullOrEmpty(eval)) { /* url = CSharpEval(...) */ } // ← строка 78–85
|
||
if (!url.StartsWith("http")) url = $"{init.host}/{url}"; // ← строка 87
|
||
}
|
||
```
|
||
|
||
Блок `format` применяется **только в ветке `else if (!string.IsNullOrEmpty(cat))`** —
|
||
то есть **не для реально первой** категории в переборе. Эмпирически: категория,
|
||
стоящая **первой** в `menu.categories`, берёт **базовый `url` из `list.firstpage`**,
|
||
и `format` для неё не срабатывает.
|
||
|
||
У меня первой стояла `Фильмы` → она читала `list.uri`/`firstpage`, а он в тот момент
|
||
указывал на общий `watchlist-films.json`, содержимое которого подтягивалось из
|
||
`watchlist.json` (все каналы, 360 позиций films).
|
||
|
||
### Фикс
|
||
|
||
**Поставить первой в `menu.categories` ту категорию, файл которой и лежит в `list.uri`.**
|
||
В рабочем `watchlist.yaml`:
|
||
|
||
```yaml
|
||
menu:
|
||
- categories:
|
||
Новинки: new # ← ПЕРВОЙ (читает list.uri = watchlist-new.json)
|
||
Фильмы: films # ← через format
|
||
Мультфильмы: cartoons
|
||
Аниме: anime
|
||
format:
|
||
"-": |
|
||
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
|
||
|
||
list:
|
||
uri: watchlist-new.json # ← согласовано с первой категорией
|
||
firstpage: watchlist-new.json
|
||
total_pages: 1
|
||
```
|
||
|
||
⚠️ **Правило на будущее:** `list.uri` и первая категория в `menu.categories`
|
||
должны указывать на **один и тот же файл**. Если переставить категории местами —
|
||
поправить `list.uri`, иначе первая категория снова отдаст чужие данные.
|
||
Этот комментарий вписан прямо в `watchlist.yaml` на HDD.
|
||
|
||
### Что ещё опробовано и НЕ сработало (не повторять)
|
||
|
||
- `?nocache={Guid.NewGuid()}` в `format` — не помогло, дело не в кэше
|
||
- Перезапуск контейнера — не помогло (кэш ни при чём, `hybridCache` в памяти процесса)
|
||
- Разбивка на 4 отдельных `menu`-записи с разными `format` — **сломало всё**, канал отдавал пустоту
|
||
- Явный `format["sort"]` — тоже путь в никуда, `sort` у меня не используется
|
||
|
||
### Артефакты, написанные в этой сессии (локально, `/Users/admin/lampac-tmp/`)
|
||
|
||
| Файл | Роль | Статус |
|
||
|---|---|---|
|
||
| `gen_watchlist_v2.py` | генератор с постерами: Radarr → TMDB → фолбэк Кинопоиск | ✅ прогнан, 330 постеров из 403 |
|
||
| `patch_plugin.py` | патч `plugin.js` (+`enumerable:true` в 2 местах) | ✅ `node --check` чист |
|
||
| `plugin-patched.js` | результат патча (10738 Б) | ✅ готов |
|
||
| `watchlist.yaml` | исправленный порядок категорий + комментарий-питфолл | ✅ готов |
|
||
| `docker-compose.yml` | + volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` | ✅ готов |
|
||
| `deploy_lampac.py` | заливка 7 шагов: бэкап → JSON → YAML → plugin → volume → restart → проверка | ✅ готов, не запущен |
|
||
| `movies-watchlist.md` | локальная копия источника (682 строки) | ✅ |
|
||
|
||
**Генерация занимает ~8–10 минут** (403 фильма × запросы к TMDB, `time.sleep(0.02)`).
|
||
Кэш: `~/.lampac_tmdb_cache.json`. Прогресс — в stderr, каждые 25 позиций.
|
||
🔴 **Запускать в background** (`notify_on_complete=true`) — foreground упрётся в 600 с.
|
||
|
||
### Питфоллы `gen_watchlist_v2.py` (найдены в работе)
|
||
|
||
1. **Хвостовой год в названии ломает поиск TMDB.**
|
||
«Простите за беспокойство **2018**», «Одиссея **2026**» — TMDB не находит.
|
||
**Фикс:** в `clean_title()` срез `\s+(?:19|20)\d{2}\)?$`, и в `tmdb_search()`
|
||
год переносится в `year`-хинт.
|
||
2. **Ключ кэша должен включать год** (`title|year`), иначе «Дракула 1992» и
|
||
«Дракула 2025» схлопнутся в одну запись.
|
||
3. **Распознавание media_type:** искать и `movie`, и `tv` (аниме/сериалы в списке есть).
|
||
|
||
### Ключевой факт про `href` в каталоге (из `ListController.cs:314–322`)
|
||
|
||
```csharp
|
||
else if (href.StartsWith("http"))
|
||
href = Regex.Replace(href, "https?://[^/]+/", ""); // вырезает домен
|
||
```
|
||
|
||
Поэтому `https://www.themoviedb.org/movie/687163` → `id = "movie/687163"` →
|
||
`CardController` получает полноценный TMDB-URI → **карточка с постером, жанрами,
|
||
описанием подтягивается штатным механизмом Lampa.** Работает для `movie/N` и `tv/N`.
|
||
|
||
### Проверка результата (то, что должен увидеть Alex)
|
||
|
||
```bash
|
||
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films&page=1" \
|
||
| jq '{total: (.results|length),
|
||
posters: ([.results[]|select(.img)]|length),
|
||
tmdb: ([.results[]|select(.id|test("^(movie|tv)/"))]|length)}'
|
||
```
|
||
|
||
|
||
|
||
## 🟢 РЕШЕНИЕ: первая категория игнорирует `format` (2026-09-22, проверено)
|
||
|
||
**Это была настоящая причина «здесь пусто» в UI.** Читать первым, если каталог отдаёт
|
||
названия, но без постеров и с `id` = ссылкой на поиск Кинопоиска.
|
||
|
||
**Симптом (замер):**
|
||
```
|
||
cat=films → 360 элементов, постеров 0, id = "index.php?kp_query=…"
|
||
cat=films&sort=rating → 1 элемент (правильный файл!)
|
||
cat=new → 33 ✅ cat=cartoons → 3 ✅ cat=anime → 7 ✅
|
||
```
|
||
Диагноз: `cat=films` читала **`watchlist.json`** (общий файл, 360 = `channels.films.items`),
|
||
а не `watchlist-films.json`. То есть **блок `format` для неё не применился**.
|
||
|
||
**Причина — по коду `ListController.cs`:**
|
||
|
||
```csharp
|
||
// строка 50: базовый URL
|
||
string url = $"{init.host}/{(page == 1 && init.list?.firstpage != null
|
||
? init.list?.firstpage : init.list?.uri)}";
|
||
|
||
// строки 59-85: ветка при непустом 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.BaseExecute<string>(eval, new CatalogGlobalsMenuRoute(...));
|
||
```
|
||
|
||
Категория, **стоящая ПЕРВОЙ** в списке `categories`, до применения `format` уже получает
|
||
базовый URL из `list.uri`. Пока `Фильмы` была первой, она читала `list.uri`
|
||
(= `watchlist.json` при том значении, что стояло) вместо своего `format`-URL.
|
||
|
||
**Фикс (одна перестановка):**
|
||
```yaml
|
||
menu:
|
||
- categories:
|
||
Новинки: new # ← первой ставим НОВИНКИ
|
||
Фильмы: films # (она читает list.uri = watchlist-new.json)
|
||
Мультфильмы: cartoons
|
||
Аниме: anime
|
||
format:
|
||
"-": |
|
||
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
|
||
```
|
||
⚠️ **`list.uri` и `list.firstpage` обязаны совпадать с файлом первой категории**
|
||
(здесь — `watchlist-new.json`), иначе первая категория отдаст чужой файл.
|
||
В генераторе это зафиксировано комментарием-питфоллом.
|
||
|
||
**Проверка после правки (все 4 должны дать свои числа, а не общий список):**
|
||
```bash
|
||
for c in new films cartoons anime; do
|
||
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=$c&page=1" \
|
||
| jq -r "\"$c: \(.results|length) элементов, постеров \([.results[]|select(.img!=null)]|length)\""
|
||
done
|
||
```
|
||
|
||
## 🔴 ГЛАВНЫЙ ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН, он генерируется сам (2026-09-22)
|
||
|
||
**Симптом:** добавил блок `main:` в `sites/watchlist.yaml` → «Мой список» **полностью
|
||
исчез** из `/catalog` (остались только Eneyida, FilmixApp, KinoUkr, Kinobase, Rezka, VoKino).
|
||
Запрос `catalog/list?plugin=watchlist` отдавал **пустоту**. В логах Lampac — **тишина**,
|
||
ошибка не пишется, сайт просто отбрасывается.
|
||
|
||
**Причина:** `CatalogSettings` **не имеет поля `main`** для входного YAML. YamlDotNet
|
||
давится неизвестным полем → `ModInit.goInit(site)` возвращает **null** →
|
||
`ApiController.cs:70` отбрасывает сайт:
|
||
```csharp
|
||
if (init == null || !init.enable || init.menu == null || init.hide)
|
||
continue;
|
||
```
|
||
|
||
⚠️ Проверено обе формы — **обе ломают**:
|
||
```yaml
|
||
main: # список — НЕ работает
|
||
- categories:
|
||
Фильмы: films
|
||
```
|
||
```yaml
|
||
main: # словарь — НЕ работает
|
||
Фильмы: http://...
|
||
```
|
||
|
||
**Как на самом деле:** Catalog **сам генерирует `main`** в JSON-ответе `/catalog`,
|
||
выводя его из блока `menu`. Проверено — в ответе есть **и `menu`, и `main`** с одинаковым
|
||
содержимым:
|
||
```json
|
||
"menu": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... },
|
||
"main": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... }
|
||
```
|
||
**Писать `main` в YAML не нужно и нельзя.** Достаточно `menu`.
|
||
|
||
### Правильная форма YAML (проверенная, рабочая)
|
||
|
||
```yaml
|
||
enable: true
|
||
jsonPath: true
|
||
displayindex: 1
|
||
displayname: Мой список
|
||
host: http://127.0.0.1:9118
|
||
|
||
menu: # ← ЕДИНСТВЕННЫЙ обязательный блок категорий
|
||
- categories:
|
||
Фильмы: films
|
||
Новинки: new
|
||
Мультфильмы: cartoons
|
||
Аниме: anime
|
||
format:
|
||
"-": |
|
||
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
|
||
|
||
search:
|
||
uri: watchlist.json
|
||
|
||
list:
|
||
uri: watchlist-films.json
|
||
firstpage: watchlist-films.json
|
||
total_pages: 1
|
||
|
||
content:
|
||
nodes: "$.items[*]"
|
||
name: { attribute: name }
|
||
original_name: { attribute: original }
|
||
year: { attribute: year }
|
||
href: { attribute: href }
|
||
image: { attribute: image }
|
||
args:
|
||
- { name_arg: rating, attribute: rating }
|
||
```
|
||
|
||
### Сопутствующие питфоллы (все проверены на живом сервере)
|
||
|
||
1. 🔴 **`menu` ОБЯЗАТЕЛЕН.** Не «подменяет разделы», а **регистрирует канал**.
|
||
`ApiController.cs:70`: без `menu` сайт отбрасывается. Убирать нельзя.
|
||
2. 🔴 **`host` + `uri` СКЛЕИВАЮТСЯ.** `ListController.cs:53`:
|
||
```csharp
|
||
string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
|
||
? init.list?.firstpage : init.list?.uri)}";
|
||
```
|
||
Значит `uri`/`firstpage` должны быть **относительными** (`watchlist-films.json`).
|
||
Полный URL даёт двойной префикс → пусто:
|
||
`http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json`
|
||
3. 🔴 **Явно указанный `format` в `menu` вызывает перезагрузку страницы при каждом
|
||
выборе категории.** Если не нужен — убрать `format` из `menu`, оставить только
|
||
`categories`. (Наблюдение Alex: «перезагружает прогрузку страницы при каждом выборе
|
||
раздела».)
|
||
|
||
### Кто на самом деле подменял родные разделы Lampa
|
||
|
||
**НЕ YAML.** Это `plugin.js` (он же `/catalog.js`):
|
||
```js
|
||
Lampa.Listener.follow('menu', function(e){
|
||
if (e.type=='action' && (e.action=='tv'||'cartoon'||'anime'||'movie'||'relise')) {
|
||
var catalog = Api.getCatalog();
|
||
if (!catalog) return;
|
||
...
|
||
Lampa.Router.call('category', {...});
|
||
e.abort(); // ← перехват клика по родному разделу
|
||
}
|
||
});
|
||
```
|
||
**Перехват срабатывает ТОЛЬКО когда выбран источник «Мой список».**
|
||
Родной CUB/tmdb не трогается, пока источник не переключён на наш.
|
||
|
||
### Как реально добиться «CUB + мой список сверху»
|
||
|
||
| Путь | Реализуемость |
|
||
|---|---|
|
||
| Блок `main` в YAML | ❌ **невозможно** — ломает парсинг |
|
||
| Оставить `menu` | ⚠️ источник работает, но разделы подменяются, пока он выбран |
|
||
| Патч `plugin.js` | ⚠️ возможно: убрать `Listener.follow('menu')` — перехвата не будет, но и секции сверху не появятся |
|
||
|
||
**Открытый вопрос для Alex:** родной CUB и «Мой список» — это **два источника Lampa**,
|
||
переключаются в «остальное → основной источник». Одновременно показывать нельзя без
|
||
патча `plugin.js`. Решение за Alex.
|
||
|
||
## ✅ РЕШЕНИЕ НАЙДЕНО 2026-09-22 — полная архитектура (проверено)
|
||
|
||
### Что уже есть в системе (нашёл, проверено)
|
||
|
||
**Модуль `watchlist_sync/resolver.py`** (`/srv/.../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/Sonarr. 🔴 **ВОТ ИСТОЧНИК СКАЧКИ:**
|
||
```python
|
||
"monitored": True,
|
||
"addOptions": {"searchForMovie": True}, # ← качает сразу после добавления
|
||
```
|
||
Для сериалов: `"addOptions": {"searchForMissingEpisodes": True}` — то же самое.
|
||
|
||
### Ключевая связка ID
|
||
|
||
| База | Где есть | Формат |
|
||
|---|---|---|
|
||
| **Кинопоиск (kp)** | в `.md`: 68 строк со ссылками `kinopoisk.ru/film/<id>/` | `11825624` |
|
||
| **TMDB** | в Radarr (`tmdbId`), в resolver (`Candidate.tmdb_id`) | `1292695` |
|
||
| **IMDb** | в Radarr (`imdbId`), в `.md` только рейтинг | `tt31728330` |
|
||
|
||
⚠️ **Прямой связи kp↔tmdb в файле нет.** Связка делается через **`resolver.py`**: он по
|
||
названию находит и `kp_id`, и `tmdb_id` одновременно. Это его основная работа.
|
||
|
||
### Замер Radarr (после шагов 1–3, 2026-09-22)
|
||
|
||
| Метрика | Значение |
|
||
|---|---|
|
||
| Фильмов | **50** |
|
||
| `monitored` | **0** ← автоскачка выключена |
|
||
| `rssSyncInterval` | **0** ← в RSS не ходит |
|
||
| Очередь | **0** |
|
||
| С `tmdbId` | 50 (все) |
|
||
| С постером | **46** |
|
||
| С рейтингом TMDB | 50 (все) |
|
||
| С файлом на диске | 24 (не трогали) |
|
||
|
||
Sonarr: 13 сериалов, `monitored` 0, RSS 0, очередь 0.
|
||
|
||
### Поля Radarr для каталога (проверено, `GET /api/v3/movie`)
|
||
|
||
```json
|
||
{
|
||
"tmdbId": 1292695,
|
||
"imdbId": "tt31728330",
|
||
"title": "They Will Kill You",
|
||
"originalTitle": "They Will Kill You",
|
||
"year": 2026,
|
||
"titleSlug": "1292695",
|
||
"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`) — уже включён.
|
||
|
||
### КАК ИГРАЕТ LAMPA (проверено — на диск НЕ пишется)
|
||
|
||
| Компонент | Значение | Смысл |
|
||
|---|---|---|
|
||
| TorrServer | `/ts/echo` → `MatriX.135` | жив, встроен в Lampac |
|
||
| **`UseDisk`** | **`false`** | 🔴 **на диск НЕ пишет** |
|
||
| `CacheSize` | 100663296 (96 МБ) | кэш в RAM |
|
||
| `TorrentsSavePath` | `""` (пусто) | пути сохранения нет |
|
||
| `TorrentDisconnectTimeout` | 180 | раздача живёт 3 мин после стопа |
|
||
| Занятые торренты | `[]` (пусто) | ничего не висит |
|
||
|
||
**Поиск торрентов:** Lampa → `jackett_url = 192.168.1.14:9118` → **Lampac сам отдаёт
|
||
Jackett-совместимый API**:
|
||
```bash
|
||
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 **не нужен** (его нет в системе).
|
||
|
||
🔴 **`parser_torrent_type = 'jackett'`** в `lampainit.js` — это НЕ отдельный Jackett,
|
||
а эндпоинт внутри Lampac. Так и задумано производителем.
|
||
|
||
### Итоговая архитектура (то, что нужно поставить)
|
||
|
||
```
|
||
family/documents/movies-watchlist.md ← источник (360 фильмов)
|
||
│
|
||
├─► resolver.py ──► TMDB + Кинопоиск
|
||
│ │ tmdb_id + постер + рейтинг + жанры
|
||
│ ▼
|
||
│ wwwroot/watchlist-<cat>.json ← каталог ДЛЯ LAMPA (с постерами!)
|
||
│ │
|
||
│ ▼
|
||
│ sites/watchlist.yaml (main: только секции СВЕРХУ, БЕЗ menu)
|
||
│ │
|
||
│ ▼
|
||
│ Lampa: плагин catalog.js → секции с постерами СВЕРХУ родного CUB
|
||
│
|
||
└─► arr.py (searchForMovie: False) ──► Radarr/Sonarr
|
||
└─► метаданные в базу БЕЗ скачки
|
||
|
||
Просмотр: Lampa → TorrServer (RAM, UseDisk:false) → на HDD ничего
|
||
```
|
||
|
||
**Разделение ролей:**
|
||
- **resolver.py** — постеры/ID для **каталога** (360 фильмов, включая те, что не в Radarr)
|
||
- **Radarr** — справочник того, что уже добавлено (50), источник `hasFile`
|
||
- **TorrServer** — воспроизведение, на диск не пишет
|
||
|
||
### Три правки (на апруве)
|
||
|
||
| # | Файл | Правка | Зачем |
|
||
|---|---|---|---|
|
||
| 1 | `watchlist_sync/arr.py` | `searchForMovie: True` → **`False`** (+ у дочки то же для сериалов) | добавление без скачки |
|
||
| 2 | ~~`catalog/sites/watchlist.yaml` — убрать блок `menu`, оставить `main`~~ ❌ **НЕВОЗМОЖНО** (опровергнуто тестом). Вместо этого — выбрать путь из трёх: оставить как есть / патч `plugin.js` / свой плагин. Решение Alex | ❌ отменено |
|
||
| 3 | генератор каталога | брать постер/ID через `resolver` | реальные элементы вместо заглушек |
|
||
|
||
⚠️ Правка №1 **может уже не понадобиться**: шаги 1–3 (unmonitor + RSS 0) закрыли скачку
|
||
на уровне состояния. Но `searchForMovie: True` в коде **включит её снова** при следующем
|
||
добавлении из списка. Поэтому фикс нужен — иначе вернётся.
|
||
|
||
## 🎯 ПЕРЕДЕЛКА 2 — Radarr как источник метаданных без автоскачки (план Alex, 2026-09-22)
|
||
|
||
### Логика Alex (дословно)
|
||
|
||
> «Мы блядь убираем автоскачку на диск так?! При этом ты оставляешь radarr/sonarr
|
||
> источником, так?? И в него из списка добавляется скриптом, но не грузит автоматом так?
|
||
> Сейчас ты отменяешь текущие загрузки, так??»
|
||
|
||
**Расшифровка:**
|
||
|
||
| # | Что | Как |
|
||
|---|---|---|
|
||
| 1 | **Автоскачка на HDD — выключить** | Radarr/Sonarr ничего не качают сами |
|
||
| 2 | **Radarr/Sonarr остаются источником** | TMDB ID + постеры для каталога Lampa |
|
||
| 3 | **Скрипт добавляет в них из списка** | `.md` → Radarr API, фильм получает метаданные |
|
||
| 4 | **Но не грузит автоматом** | Добавление ≠ загрузка |
|
||
| 5 | **Текущие загрузки — отменить** | 6 записей в очереди |
|
||
|
||
### Как это в Radarr технически (проверено 2026-09-22)
|
||
|
||
⚠️ **Важно для понимания:** в Radarr «добавить, но не качать» = **`monitored: false`**.
|
||
Отдельного состояния «источник, но не грузить» нет. При добавлении через API Radarr
|
||
**сам ходит в TMDB** и тянет метаданные (постер, год, описание) — даже при `monitored: false`.
|
||
Это и есть решение задачи №2+№3+№4.
|
||
|
||
### Доступ к Radarr API (проверено 2026-09-22, воспроизводимо)
|
||
|
||
```bash
|
||
# API-ключ лежит в config.xml (не в БД)
|
||
ssh kraken 'grep -oE "<ApiKey>[^<]+</ApiKey>" \
|
||
/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker/radarr/config/config.xml' \
|
||
| sed 's/<[^>]*>//g' # → cbcb8ec320104359920c0f611d503c7b
|
||
```
|
||
|
||
🔴 **ПИТФОЛЛ: `-L` обязателен.** Без него Radarr отдаёт **307 с пустым телом** и `jq length` падает:
|
||
|
||
```bash
|
||
curl -s "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # → пустой вывод, 0 байт
|
||
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # → 200, 50
|
||
```
|
||
|
||
**Конфиг Radarr (замер):**
|
||
|
||
```bash
|
||
curl -sL ".../api/v3/config/indexer?apikey=$KEY" | jq .rssSyncInterval # → 30 (мин)
|
||
```
|
||
|
||
`rssSyncInterval: 30` = Radarr **каждые 30 минут** сам ходит в RSS-фиды и качает новое.
|
||
Это вторая причина автоскачки помимо `monitored: true`. **Выключать надо оба.**
|
||
|
||
### Окружение Radarr на Кра́кене (для будущих сессий)
|
||
|
||
| Параметр | Значение |
|
||
|---|---|
|
||
| Порт | `7878` (`0.0.0.0`) |
|
||
| Контейнер | `radarr`, образ `lscr.io/linuxserver/radarr:latest` |
|
||
| Конфиг на HDD | `/srv/...6194539b.../docker/radarr/config/` |
|
||
| БД | `radarr.db` (+ `-shm`, `-wal`) |
|
||
| Постеры | `/config/MediaCover/` — **46 каталогов** |
|
||
| `sqlite3` в контейнере | ❌ **нет** (на хосте Кра́кена — **есть**, `/usr/bin/sqlite3`) |
|
||
| Соседи | `sonarr:8989`, `prowlarr:9696`, `transmission` |
|
||
|
||
⚠️ В контейнерах `sqlite3` отсутствует (см. профиль Alex) — БД читать через `strings`
|
||
или вытаскивать файл наружу и смотреть хостовым `sqlite3`.
|
||
|
||
### Текущее состояние Radarr (замер 2026-09-22)
|
||
|
||
| Метрика | Значение |
|
||
|---|---|
|
||
| Фильмов всего | 50 |
|
||
| `monitored: true` | **45** ← активны, ищут и качают |
|
||
| `hasFile: true` | 24 (уже на диске) |
|
||
| `rssSyncInterval` | **30** мин ← Radarr ходит в RSS каждые полчаса |
|
||
| Очередь | **6 записей** (все `warning`, ничего не качается активно) |
|
||
| Transmission | простаивает (1% CPU) |
|
||
|
||
Очередь (id для отмены):
|
||
|
||
```
|
||
598407147 Brokeback Mountain.mkv
|
||
1092005446 Гладиатор (2000) (DTS X) DV.mkv
|
||
914899324 Sorry.to.Bother.You.BDRemux.1080p.NNMClub.pk.mkv
|
||
1671545331 Мортал Комбат 2 (DTS-HD MA).mkv
|
||
1486734395 Gorod.boga.2002.BDRemux.1080p.mkv
|
||
139445264 День хомячка (2003)
|
||
```
|
||
|
||
## ✅ ПРОВЕРКА «как это реализуемо» — полный аудит 2026-09-22 (вечер)
|
||
|
||
Alex: «Мне надо чтоб lampa источники видела и играла фильмы из списка и cub.
|
||
При этом мы перестаем качать торренты на диск! Как это реализуемо??
|
||
Проверь блядь все чтобы ты опять не обосрался».
|
||
|
||
Проверено всё, что может качать или мешать. **Ничего не менялось — только чтение.**
|
||
|
||
### 🎯 Главная находка: TorrServer на диск НЕ пишет
|
||
|
||
Ключевой факт, снимающий главный вопрос Alex («при этом фильмы не будут
|
||
скачиваться на hdd я правильно понимаю?!»). Конфиг TorrServer внутри Lampac:
|
||
|
||
```bash
|
||
ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq .
|
||
```
|
||
|
||
```jsonc
|
||
{
|
||
"BitTorr": {
|
||
"CacheSize": 100663296, // 96 МБ — RAM-кэш
|
||
"UseDisk": false, // 🔴 НА ДИСК НЕ ПИШЕТ
|
||
"TorrentsSavePath": "", // путь сохранения пуст
|
||
"TorrentDisconnectTimeout": 180, // раздача живёт 3 мин после остановки
|
||
"PreloadCache": 0,
|
||
"ReaderReadAHead": 83
|
||
}
|
||
}
|
||
```
|
||
|
||
**`UseDisk: false`** — TorrServer держит всё в RAM (96 МБ) и **на диск не пишет ничего**.
|
||
Это ровно то, что нужно Alex. Настраивать не надо, уже так.
|
||
|
||
### TorrServer жив и работает (проверено)
|
||
|
||
```bash
|
||
curl -s "http://192.168.1.14:9118/ts/echo" # → MatriX.135 (версия, живой)
|
||
curl -s "http://192.168.1.14:9118/ts/torrents" # → 404 (это не тот эндпоинт)
|
||
curl -s -X POST "http://192.168.1.14:9118/ts/torrents" \
|
||
-H 'Content-Type: application/json' -d '{"action":"list"}' # → [] пусто, ничего не висит
|
||
```
|
||
|
||
Кэш Lampac: `/lampac/cache/` = 31 МБ (SQLite, Playwright, gstreamer-registry) —
|
||
**не фильмы**. `/lampac/data/` = 44 МБ (ts/settings.json, dlna/thumbs). Диск не забит.
|
||
|
||
### Поиск торрентов — Jackett внутри Lampac, отдельный НЕ нужен
|
||
|
||
🔴 **ПИТФОЛЛ-ЛОВУШКА: в `lampainit.js` стоит `parser_torrent_type = 'jackett'`,
|
||
но отдельного контейнера Jackett НЕТ.** Это не ошибка — Lampac **сам отдаёт
|
||
Jackett-совместимый API**.
|
||
|
||
```bash
|
||
# что прописывает Lampac клиенту:
|
||
curl -s "http://192.168.1.14:9118/lampainit.js" | grep -oE "Lampa.Storage.set\('[^']+', ?'[^']*'\)"
|
||
# Lampa.Storage.set('jackett_url', '192.168.1.14:9118')
|
||
# Lampa.Storage.set('jackett_key', '1')
|
||
# Lampa.Storage.set('parser_torrent_type', 'jackett')
|
||
# Lampa.Storage.set('parser_use', 'true')
|
||
```
|
||
|
||
`jackett_url` указывает **на сам Lampac**, а не на сторонний сервис. Проверка API:
|
||
|
||
```bash
|
||
# ⚠️ работает БЕЗ префикса /jackett — на корне
|
||
curl -s "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix"
|
||
# → 200, реальные результаты: rutor, магнит-ссылки, Size, Seeders, ffprobe
|
||
|
||
curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.14:9118/jackett/api/v2.0/..." # → 404
|
||
curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.14:9118/api/v2.0/..." # → 200 ✅
|
||
```
|
||
|
||
**Вывод:** поиск торрентов работает штатно, ничего поднимать не надо.
|
||
|
||
### Кто качает на диск — полный реестр (единственные виновники)
|
||
|
||
```bash
|
||
ssh kraken 'docker ps --format "{{.Names}}\t{{.Image}}"'
|
||
```
|
||
|
||
| Сервис | Качает? | Замер | Нужен для задачи? |
|
||
|---|---|---|---|
|
||
| **Radarr** | ✅ да | 50 фильмов, **45 monitored**, очередь **6**, RSS 30 мин | ❌ мешает |
|
||
| **Sonarr** | ✅ да | 13 сериалов, **13 monitored**, очередь **19** | ❌ мешает |
|
||
| **Transmission** | ⚠️ исполнитель | **257** `.torrent`, CPU 0.76% (простаивает), NetIO 290G/26.7G | ⚠️ движок Radarr/Sonarr |
|
||
| **TorrServer** | ❌ НЕТ | `UseDisk: false`, список торрентов `[]` | ✅ **вот чем играет Lampa** |
|
||
|
||
Полный список контейнеров Кра́кена:
|
||
`lampac · radarr · jellyfin · transmission · portainer · flaresolverr ·
|
||
prowlarr · sonarr · watchtower · rclone · xray-reverse-bridge · hermes-kraken · cloudflared`
|
||
|
||
**Sonarr API** (для будущей работы, по аналогии с Radarr):
|
||
|
||
```bash
|
||
# ключ: /srv/.../docker/sonarr/config/config.xml → 15fbec32f1cb4b05b1c2b229ce27359b
|
||
curl -sL "http://localhost:8989/api/v3/series?apikey=$SONARR_KEY" | jq 'length' # → 13
|
||
curl -sL "http://localhost:8989/api/v3/queue?apikey=$SONARR_KEY" | jq '.totalRecords' # → 19
|
||
```
|
||
|
||
⚠️ Sonarr — **тоже источник автоскачки**, не только Radarr. Alex упомянул его не зря.
|
||
Отключать надо оба, иначе Sonarr продолжит качать сериалы.
|
||
|
||
### Transmission — настройки (замер)
|
||
|
||
```bash
|
||
ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.json
|
||
```
|
||
|
||
```json
|
||
{"download-dir": "/downloads", "rpc-port": 9091,
|
||
"speed-limit-down-enabled": false, "peer-port": 51413,
|
||
"alt-speed-enabled": false}
|
||
```
|
||
|
||
`/config/torrents/*.torrent` = **257 файлов** — это накопленный список раздач,
|
||
**не признак активной качки** (CPU 0.76%). Файлы на диске не трогаем.
|
||
|
||
### Кто чем играет в Lampa (подтверждено)
|
||
|
||
```bash
|
||
curl -s "http://192.168.1.14:9118/lampainit.js" | grep -iE "torrents_use|source"
|
||
# lampa_settings.torrents_use = true ← кнопка торрентов включена
|
||
# Lampa.Storage.set('source', 'cub') ← CUB — основной источник ✅
|
||
# Lampa.Storage.set('proxy_tmdb', 'true')
|
||
```
|
||
|
||
В `plugins_add` (серверные плагины Lampac, не список Lampa):
|
||
`tmdbproxy.js · cubproxy.js · online.js · catalog.js · sisi.js · startpage.js ·
|
||
timecode.js · ts.js` — все `status: 1`.
|
||
|
||
### Итоговая схема (что получится после переделки)
|
||
|
||
```
|
||
ТВ: Lampa (com.lampac.tv)
|
||
├── источник CUB ── онлайн, работает как раньше ✅
|
||
└── источник «Мой список» ── секции СВЕРХУ, постеры из Radarr
|
||
│
|
||
▼ клик по фильму
|
||
поиск торрента через Lampac (Jackett-API на :9118)
|
||
│
|
||
▼
|
||
TorrServer (UseDisk: false) → RAM-кэш 96 МБ → на HDD НЕ ПИШЕТ ✅
|
||
|
||
Radarr/Sonarr после переделки = ЧИСТЫЙ СПРАВОЧНИК МЕТАДАННЫХ
|
||
(monitored: false + rssSyncInterval: 0) — не ищут, не качают
|
||
```
|
||
|
||
### ✅ Порядок работ — ШАГИ 0–3 ВЫПОЛНЕНЫ 2026-09-22
|
||
|
||
| # | Действие | Сервис | Статус |
|
||
|---|---|---|---|
|
||
| 0 | Бэкап `radarr.db` + `config.xml` + `sonarr.db` + `config.xml` | оба | ✅ **сделано** |
|
||
| 1 | Отменить очередь: 6 Radarr + 19 Sonarr | оба | ✅ **сделано** (оба 0) |
|
||
| 2 | `monitored: false` — 45 Radarr + 13 Sonarr | оба | ✅ **сделано** (оба 0) |
|
||
| 3 | `rssSyncInterval: 0` | оба | ✅ **сделано** (оба 0) |
|
||
| 4 | Проверка: monitored 0, очередь 0, RSS 0, Transmission тих | оба | ✅ **сделано** |
|
||
| 5 | Генератор каталога → постеры/ID через **TMDB + Radarr** (`gen_watchlist_v2.py`) | — | ✅ **написан, прогнан** (330/403 постеров) |
|
||
| 6 | ~~В `sites/watchlist.yaml` убрать `menu`, оставить `main`~~ ❌ **ОТМЕНЕНО** — невозможно, см. «🔴 ПИТФОЛЛ YAML» | — | ❌ опровергнуто |
|
||
| 6b | **Порядок категорий в `watchlist.yaml`:** «Новинки» первой, `list.uri` = `watchlist-new.json` | — | ✅ **исправлено** |
|
||
| 7 | Фикс `plugin.js` (`enumerable:true`) | — | ✅ **файл пропатчен**, заливка ждёт |
|
||
| 8 | Фикс `arr.py`: `searchForMovie` → `False` | — | ✅ **сделано, коммит `9872016`** |
|
||
| 9 | Заливка на Кра́кен + пересоздание контейнера + приёмка в UI | — | ⏳ `deploy_lampac.py` готов, не запущен |
|
||
|
||
**Не трогаем:** Prowlarr, Transmission, 24 файла на диске, Jellyfin, остальные контейнеры.
|
||
|
||
### 📋 Как выполнены шаги 1–3 (скрипты и команды — воспроизводимо)
|
||
|
||
**Шаг 0 — бэкапы (метка `20260922-123404`)**
|
||
```bash
|
||
D=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker
|
||
TS=$(date +%Y%m%d-%H%M%S)
|
||
sudo cp $D/radarr/config/radarr.db $D/radarr/config/radarr.db.bak-$TS
|
||
sudo cp $D/radarr/config/config.xml $D/radarr/config/config.xml.bak-$TS
|
||
sudo cp $D/sonarr/config/sonarr.db $D/sonarr/config/sonarr.db.bak-$TS
|
||
sudo cp $D/sonarr/config/config.xml $D/sonarr/config/config.xml.bak-$TS
|
||
```
|
||
Результат: `radarr.db` 4 374 528 Б, `sonarr.db` 4 870 144 Б + оба `config.xml` 650/648 Б.
|
||
|
||
**Шаг 1 — очистка очереди.** Скрипт `/tmp/arr_clear_queue.py` (на Mac → `scp` → запуск на Кра́кене).
|
||
`DELETE /api/v3/queue/<id>?removeFromClient=true&blocklist=false`.
|
||
Результат: Radarr 6 → **0**, Sonarr 19 → **0**.
|
||
|
||
**Шаг 2 — снятие monitored.**
|
||
- Radarr: `PUT /api/v3/movie/editor` с ключом **`movieIds`** → 45 → **0**
|
||
- Sonarr: `PUT /api/v3/series/editor` с ключом **`seriesIds`** → 13 → **0**
|
||
|
||
Скрипты: `/tmp/arr_disable_autograb.py` (Radarr, упал на Sonarr с HTTP 500),
|
||
`/tmp/sonarr_unmonitor.py` (Sonarr, исправленный ключ).
|
||
|
||
**Шаг 3 — `rssSyncInterval: 0`.** Скрипт `/tmp/radarr_rss_off.py`.
|
||
Sonarr: 15 → 0 с первого раза. Radarr: **со второго** — первый заход вернул 30
|
||
(не отправлял `id`), фикс — отправлять **весь объект конфига с `id: 1`**.
|
||
|
||
### 📊 Состояние ПОСЛЕ выполнения (замер 2026-09-22)
|
||
|
||
| Метрика | Radarr | Sonarr |
|
||
|---|---|---|
|
||
| Всего записей | 50 фильмов | 13 сериалов |
|
||
| `monitored` | **0** (было 45) | **0** (было 13) |
|
||
| `rssSyncInterval` | **0** (было 30) | **0** (было 15) |
|
||
| Очередь | **0** (было 6) | **0** (было 19) |
|
||
| С `tmdbId` | 50 (все) | — |
|
||
| С постером | **46** | — |
|
||
| С рейтингом TMDB | 50 (все) | — |
|
||
| С файлом на диске | 24 (не трогали) | — |
|
||
|
||
**Проверка пассивности:** Transmission за 45 секунд — NET `290GB/26.7GB` и
|
||
BLOCK `214GB/393kB` **не сдвинулись ни на байт**. Логи Radarr (`grep search|download|grab`)
|
||
за 2 минуты — **пусто**. TorrServer: список торрентов `[]`, `echo` → `MatriX.135`.
|
||
|
||
### 🔴 Питфоллы выполнения (найденные в этой сессии)
|
||
|
||
1. **`urllib` НЕ идёт по 307 автоматически, а `curl -sL` — идёт.**
|
||
Симптом: все 25 DELETE в первом прогоне вернули `HTTP Error 307: Temporary Redirect`,
|
||
**ничего не удалилось**, состояние не изменилось.
|
||
**Фикс:** свой `HTTPRedirectHandler` подкласс, который для 307/308 сохраняет
|
||
**метод и тело** (штатный `urllib` превращает DELETE в GET):
|
||
```python
|
||
class RedirectKeepMethod(urllib.request.HTTPRedirectHandler):
|
||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||
if code in (307, 308):
|
||
new = urllib.request.Request(newurl, data=req.data,
|
||
method=req.get_method())
|
||
for k, v in req.headers.items():
|
||
new.add_header(k, v)
|
||
return new
|
||
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
||
```
|
||
Тот же корень, что у `curl` без `-L` (питфолл в памяти) — только на стороне Python.
|
||
|
||
2. **Sonarr группирует эпизоды сезона под одним `id`.**
|
||
Симптом: после удаления первой записи остальные возвращают `HTTP 404: Not Found`.
|
||
Это **не сбой** — они исчезли вместе с родительской. Итог всё равно 0.
|
||
|
||
3. **Ключ поля в `*/editor` различается между Radarr и Sonarr.**
|
||
Radarr `/movie/editor` → **`movieIds`**; Sonarr `/series/editor` → **`seriesIds`**.
|
||
`ids` → HTTP 500 Internal Server Error (Sonarr).
|
||
|
||
4. **`PUT /api/v3/config/indexer` требует весь объект с `id`.**
|
||
Частичный `{"rssSyncInterval": 0}` молча возвращает старое значение.
|
||
Отправлять весь объект, полученный `GET`, с изменённым полем.
|
||
|
||
5. **Кириллица в inline-`jq` внутри `ssh '...'`** снова сломала команду
|
||
(`unexpected INVALID_CHARACTER`) — 6 ошибок в одном вызове. Ключи объекта
|
||
писать латиницей (`total`, `monitored`, `queue`), не `всего`/`очередь`.
|
||
|
||
### ❓ Открытый вопрос к Alex — ЗАКРЫТ
|
||
|
||
Был вопрос «Radarr/Sonarr нужны как справочник или выключаются совсем?».
|
||
**Alex ответил резко:** «Как ты блядь думаешь мне нужны блядь постеры или нет блядь?!
|
||
Меня не ебет откуда оно их берет сделай чтобы работало как поставлено в задаче».
|
||
→ **Остаются как справочник метаданных.** Вопрос постеров больше не задавать.
|
||
|
||
### План работ (на апруве Alex) — ШАГИ 0–4 ВЫПОЛНЕНЫ
|
||
|
||
**Шаг 0 — бэкап (обязательно, до всего)**
|
||
```bash
|
||
ssh kraken 'cp /srv/.../docker/radarr/config/radarr.db /srv/.../radarr.db.bak-$(date +%Y%m%d-%H%M%S)'
|
||
ssh kraken 'cp /srv/.../docker/radarr/config/config.xml /srv/.../config.xml.bak-$(date +%Y%m%d-%H%M%S)'
|
||
```
|
||
Причина: `radarr.db` — боевая база, содержит всю библиотеку.
|
||
|
||
**Шаг 1 — отменить текущие загрузки (6 шт)**
|
||
```bash
|
||
curl -sL -X DELETE "http://localhost:7878/api/v3/queue/<id>?removeFromClient=true&blocklist=false&apikey=$KEY"
|
||
```
|
||
⚠️ **Не** использовать `blocklist=true` — иначе релизы попадут в чёрный список и
|
||
не найдутся при ручной загрузке. Файлы на диске **не трогаются**.
|
||
|
||
**Шаг 2 — снять `monitored` со всех фильмов**
|
||
```bash
|
||
curl -sL -X PUT "http://localhost:7878/api/v3/movie/editor?apikey=$KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"movieIds":[<все 50>],"monitored":false}'
|
||
```
|
||
Результат: Radarr не ищет и не качает. Фильмы в базе с метаданными остаются.
|
||
|
||
**Шаг 3 — выключить авто-поиск по RSS**
|
||
```bash
|
||
curl -sL -X PUT "http://localhost:7878/api/v3/config/indexer?apikey=$KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"rssSyncInterval":0,"id":1, ...остальные поля как есть}'
|
||
```
|
||
0 = Radarr не ходит в RSS. Ничего не находит сам.
|
||
|
||
**Шаг 4 — проверить**
|
||
```bash
|
||
jq '[.[] | select(.monitored==true)] | length' # → 0
|
||
jq '.rssSyncInterval' # → 0
|
||
jq '.totalRecords' # очередь → 0
|
||
```
|
||
|
||
**Что НЕ трогаем:** Prowlarr, Transmission, библиотеку на диске (24 файла), Sonarr
|
||
(отдельная проверка ниже).
|
||
|
||
### ⚠️ Незакрытые вопросы к плану
|
||
|
||
1. **Sonarr** — Alex упомянул его вместе с Radarr. Проверить его настройки отдельно
|
||
(сериалы в списке есть?). Это отдельная задача. **Вопрос задан Alex, ответа нет.**
|
||
2. **Ручная загрузка после отключения** — если Alex захочет скачать фильм вручную,
|
||
нужно временно включить `monitored` или искать вручную через Interactive Search.
|
||
Работает, но не автоматически.
|
||
3. **310 фильмов, которых нет в Radarr** — скрипт будет добавлять их с `monitored: false`,
|
||
Radarr сам подтянет постеры из TMDB. Но часть названий найдётся криво («девять»).
|
||
|
||
### ✅ Подтверждено Alex 2026-09-22 (семантика задачи — не переспрашивать)
|
||
|
||
Alex сам сформулировал схему и подтвердил её понимание ассистентом:
|
||
|
||
> «При этом фильмы не будут скачиваться на hdd я правильно понимаю?! Играть будет
|
||
> через torrserver так?» → ответ: **да, правильно**.
|
||
|
||
**Разделение обязанностей (запомнить, чтобы не путать):**
|
||
|
||
| Слой | Роль | Куда пишет |
|
||
|---|---|---|
|
||
| **Radarr/Sonarr + Transmission** | библиотека, автоскачка релизов | на HDD (библиотека) |
|
||
| **Lampac TorrServer** | стриминг по требованию из UI Lampa | **ничего не сохраняет** — только временный кэш кусков |
|
||
| **«Мой список» (Catalog)** | только показ секций с постерами | ничего не качает |
|
||
|
||
То есть после выключения автоскачки в Radarr: **фильмы, которые Alex смотрит через Lampa,
|
||
на HDD не попадают вообще** — TorrServer отдаёт поток и выбрасывает буфер. Файлы на диске
|
||
остаются только у 24 уже скачанных (нужны Jellyfin).
|
||
|
||
⚠️ **`transmission` НЕ связан с Lampa.** Radarr качает через него релизы в свою библиотеку —
|
||
это отдельный контур. Вопрос «через что играет TorrServer» и «что качает Radarr» —
|
||
**разные вещи**, не смешивать в объяснениях.
|
||
|
||
### 🔴 Правка `plugin.js` (`enumerable:true`) — всё ещё НЕ применена
|
||
|
||
Независимая от ПЕРЕДЕЛКИ 2 правка. Файл лежит локально `/tmp/catalog-plugin.js`
|
||
(10738 байт, `node --check` чистый, 2 вхождения исправлены), бэкап
|
||
`/tmp/catalog-plugin.js.bak-20260922-121452`. На Кра́кене и в образе — **оригинал**.
|
||
Нужен volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` в compose.
|
||
Подробности — раздел «🔴 ГЛАВНЫЙ ПИТФОЛЛ» выше.
|
||
|
||
**Порядок:** правку `plugin.js` имеет смысл применять **вместе с ПЕРЕДЕЛКОЙ 2**,
|
||
чтобы не пересоздавать контейнер дважды.
|
||
|
||
### Откат
|
||
|
||
```bash
|
||
# вернуть monitored (одним запросом)
|
||
curl -sL -X PUT ".../api/v3/movie/editor" -d '{"movieIds":[...],"monitored":true}'
|
||
# вернуть RSS
|
||
curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1}'
|
||
# восстановить базу при необходимости
|
||
cp radarr.db.bak-<дата> radarr.db && docker restart radarr
|
||
```
|
||
|
||
**Статус: план на апруве. Ничего не выполнено.**
|
||
|
||
## Открытые вопросы — БЛОКИРУЮЩИЕ (2026-09-22, ждут решения Alex)
|
||
|
||
1. **Что делать с 310 фильмами, которых нет в Radarr?**
|
||
В `.md` — 360, в Radarr — 50. У 310 нет TMDB ID и постера.
|
||
Варианты: (а) показывать только 50 из Radarr; (б) добирать остальные через TMDB-search
|
||
(часть названий найдётся криво: «девять» → не то); (в) показывать заглушкой без постера.
|
||
**Без решения шаг 1 переделки не начать.**
|
||
2. **Нужны 4 секции или одна?** Секции «Фильмы / Новинки / Мультфильмы / Аниме»
|
||
или одна «Мой список» сверху. Вопрос задан, ответа нет.
|
||
|
||
## Открытые вопросы (не блокируют)
|
||
|
||
1. **Постеры** — тянуть через TMDB-кэш Lampac, через Radarr `MediaCover/`, или `remoteUrl` CDN?
|
||
2. **Карточка** — `card_parse` не настроен. С реальным TMDB ID карточка должна
|
||
подтянуться штатным механизмом Lampa — проверить после переделки.
|
||
3. **`🗓️` upcoming** — показывать или прятать? Сейчас показываем.
|
||
4. **`❓` ambiguous** — сейчас показываем с названием как есть.
|
||
|
||
## ⚠️ Отменённые/ошибочные указания (не повторять)
|
||
|
||
- ❌ **«Настройки → Источник»** — такого пункта в Lampa **не существует**. Выдумано,
|
||
Alex не нашёл. Источник переключается в разделе «остальное → основной источник»
|
||
(Alex нашёл сам 2026-09-22).
|
||
- ❌ **«влево → панель источников»** — тоже не подтверждено.
|
||
- ❌ **`plugins_add` в `lampainit.js`** — это серверные плагины Lampac
|
||
(`tmdbproxy.js`, `cubproxy.js`, `online.js`, `catalog.js`…), а **не** список плагинов Lampa.
|
||
`catalog.js` там есть при `"catalog": true` в init.conf.
|
||
|
||
## Связанные
|
||
|
||
- [[personal/plans/lampac-kraken]] — базовое развёртывание (шаги 1–6 сделаны)
|
||
- [[personal/tech/lampac-deployment]] — эксплуатация Lampac, модуль Catalog
|
||
- [[family/how-to/tv-lampac-plugin]] — подключение плагина на ТВ
|
||
- [[family/projects/watchlist-sync]] — парсер списка, CLI
|
||
- [[family/projects/watchlist-automation]] — форматы маркеров, cron
|
||
- [[family/documents/movies-watchlist]] — источник данных
|
||
|
||
## ✅ РАБОТАЕТ (2026-09-22, конец дня)
|
||
|
||
**Карточка фильма починена. Проверено: 339 из 339 карточек → HTTP 200.**
|
||
|
||
### Итог по каталогу
|
||
|
||
| Секция | Элементов | Постеров | Рабочих TMDB-ID |
|
||
|---|---|---|---|
|
||
| Новинки | 33 | 30 | 30 |
|
||
| Фильмы | 360 | 308 | 312 |
|
||
| Мультфильмы | 3 | 3 | 3 |
|
||
| Аниме | 7 | 7 | 7 |
|
||
| **Всего** | **403** | **348** | **352** |
|
||
|
||
Источники метаданных: Radarr 9, TMDB 343, Кинопоиск-фолбэк 51.
|
||
|
||
### Что сделано
|
||
|
||
1. Генератор `gen_watchlist_v2.py` — пишет **файл на каждый фильм**:
|
||
`wwwroot/cards/card-movie-NNN.json`, владелец `1000:1000`, режим 644.
|
||
2. `watchlist.yaml` → `card_parse.initUrl` собирает путь из `uri`:
|
||
`return $"{host}/cards/card-{uri.Replace("/", "-")}.json";`, `node: card`.
|
||
3. `deploy_lampac.py` — добавлен шаг `2b` (заливка `cards/` через tar), плюс
|
||
проверка карточки в шаге 7.
|
||
4. `arr.py` — `monitored: False`, `searchFor*: False` (коммит `9872016`).
|
||
5. `plugin.js` — патч `enumerable:true`, смонтирован volume, переживает обновление образа.
|
||
|
||
### 🔴 ПИТФОЛЛЫ, найденные по ходу (все стоили времени)
|
||
|
||
1. **Проверять карточку надо по `/catalog/card`, НЕ по `/card`.**
|
||
`/card` отдаёт 404 и выглядит как поломка каталога. Потерян час на ложном диагнозе.
|
||
2. **`parse.node` — СТАТИЧНЫЙ JSONPath.** `{uri}`/`{cat}` в него НЕ подставляются
|
||
(`json.SelectToken(parse.node)`). Слэш в ключе `movie/NNN` трактуется как вложенность
|
||
JSON → `SelectToken` возвращает null → `BadRequest("parse.node")`.
|
||
Именно поэтому выбран формат «файл на фильм» с фиксированным `$.card`.
|
||
3. **`card_parse.initUrl` ИСПОЛНЯЕТСЯ как C#** (`CSharpEval`, объект
|
||
`CatalogInitUrlCard` с полями `host/uri/type/args`). Только здесь можно подставить
|
||
`uri`. Кавычки внутри YAML-heredoc писать экранированно.
|
||
4. **`CardController.Index(plugin, uri, type)` — параметра `cat` НЕТ.**
|
||
`{cat}` в карточке всегда пустой.
|
||
5. **macOS `tar` кладёт в архив AppleDouble-мусор `._*`** — файлы попадают в
|
||
`wwwroot` и мешают. Использовать `tar --no-mac-metadata` + `rm -f ._*`.
|
||
6. **Права решают.** Если файлы отдать `root:root 600` — dotnet отвечает
|
||
`Permission denied`, статика молча отдаёт **404**. Нужно `1000:1000` + `755`/`644`.
|
||
7. **`find -exec` через `ssh -t` рвётся**: `missing argument to -exec`.
|
||
Использовать `chmod -R` отдельными вызовами.
|
||
8. **Счётчик TMDB-ID врал нулём**: `id` приходит как `movie/424781` (префикс),
|
||
а проверялось `"/movie/"` (вхождение со слэшем спереди). Проверять `startswith`.
|
||
9. **Первая категория в `menu.categories` игнорирует `format`** и берёт `list.uri`
|
||
напрямую. Поэтому «Новинки» стоят первыми, и их `list.uri = watchlist-new.json`.
|
||
10. **Ключи карточки.** `CardController` отдаёт `title`/`original_title`, а
|
||
`card_parse` ищет `name`/`original`. В JSON пишем **оба** набора ключей.
|
||
|
||
### Воспроизведение
|
||
|
||
```bash
|
||
cd /Users/admin/lampac-tmp
|
||
python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
|
||
python3 deploy_lampac.py /tmp/wl-out --skip-plugin # ~2 мин
|
||
```
|
||
|
||
Cron: генератор в **02:30**, после `watchlist-nightly` (01:00).
|
||
|
||
### Осталось на ТВ
|
||
|
||
Проверить глазами в Lampa на `192.168.1.70`: секции сверху с постерами,
|
||
клик → карточка с описанием → воспроизведение через TorrServer.
|
||
Приёмка = результат виден в UI.
|
||
|
||
### Не сделано (ждёт решения, без апрува не трогать)
|
||
|
||
310 фильмов из списка, которых нет в Radarr — пока показываются через фолбэк
|
||
Кинопоиска (51 без постера). Решить судьбу отдельно.
|
||
|
||
---
|
||
|
||
## 🔴 ФИНАЛЬНЫЙ ФИКС: карточка = «здесь пусто» без ЧИСЛОВОГО `tmdb_id`
|
||
|
||
**2026-09-22, третья сессия. Карточка заработала: 352/352 отдают `tmdb_id`.**
|
||
|
||
Alex после первого «готово»: «при открытии все так же "здесь пусто" и никакого инфо
|
||
или воспроизведения». Сервер отдавал `HTTP 200` с title/img/overview — значит
|
||
дело было **не в запросе, а в полноте ответа**.
|
||
|
||
### Причина — одна, но скрытая за двумя масками
|
||
|
||
`plugin.js` при клике (`full()`):
|
||
|
||
```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 считает карточку неполной → пустой экран.** Причём нужен
|
||
**числовой** `tmdb_id`, не строка.
|
||
|
||
Два бага в цепочке `card_args`:
|
||
|
||
1. **`ModInit.cs:285` — `nodeValue` возвращает ЛЮБОЕ `JValue` как строку.**
|
||
```csharp
|
||
string value = valueToken switch {
|
||
JValue jValue => jValue.Value?.ToString(), // ← число 424781 → "424781"
|
||
... };
|
||
```
|
||
Поэтому `tmdb_id` из JSON-файла приходил строкой, и `if(json.tmdb_id)` не
|
||
давала числа. **Никакой конфиг это не обходит — только `format`.**
|
||
|
||
2. **Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ процесса и переживает
|
||
`docker restart`.** После правки данных старые ответы продолжали отдаваться.
|
||
Именно поэтому `movie/424781` (**первый элемент «Фильмов»**, по нему Alex и
|
||
кликал) упорно оставался `null`, хотя исправленный файл уже лежал на диске.
|
||
**Проверять фикс только на НЕзакэшированном `uri`.**
|
||
|
||
### Фикс — `format` в `card_args` (исполняется как C#, возвращает настоящий тип)
|
||
|
||
```yaml
|
||
card_args:
|
||
- name_arg: tmdb_id
|
||
attribute: tmdb_id
|
||
format: "long.TryParse(value, out long v) ? v : 0"
|
||
- name_arg: source
|
||
attribute: media_type
|
||
- name_arg: overview
|
||
attribute: overview
|
||
- name_arg: genres
|
||
attribute: genres
|
||
- 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 показывает в бейдже
|
||
```
|
||
|
||
`format` вызывается как `CSharpEval.Execute<object>(nd.format,
|
||
new CatalogNodeValue(value, host), options)` (`ModInit.cs:255` и `:317`) —
|
||
`value` это уже строка, поэтому нужен `TryParse`.
|
||
|
||
### Эталонный рабочий ответ
|
||
|
||
```json
|
||
{ "id": "movie/424781", "title": "Простите за беспокойство",
|
||
"original_title": "Простите за беспокойство", "tmdb_id": 424781,
|
||
"source": "movie", "runtime": 111, "rating": "6.8", "vote_average": 68,
|
||
"genres": [{"name":"фэнтези"},{"name":"фантастика"},{"name":"комедия"}],
|
||
"overview": "...", "img": "http://192.168.1.14:9118/proxyimg:210:0/..." }
|
||
```
|
||
|
||
### Итог проверки
|
||
|
||
**352 из 352** карточек (все, у кого есть TMDB ID) отдают числовой `tmdb_id`,
|
||
`source`, `runtime`, рейтинг и жанры. Ошибок — 0.
|
||
|
||
### 🔴 CUB: «вижу только фильмы из списка»
|
||
|
||
CUB **не пропал** — это **два независимых источника Lampa**, одновременно
|
||
показать нельзя. Переключение: **Настройки → Остальное → Основной источник**.
|
||
Пункта «Источник» в настройках НЕ существует.
|
||
|
||
### Питфолл приёмки на ТВ
|
||
|
||
Lampa на ТВ **кэширует манифест и ответы**. После правок надо **полностью
|
||
выгрузить приложение** (не свернуть), иначе клиент держит старый кэш и
|
||
показывает дореформенный результат.
|
||
|
||
---
|
||
|
||
## 🔴 СЕССИЯ 4 (2026-09-22, поздняя ночь): приёмка НЕ пройдена, требование изменено
|
||
|
||
### Что сказал Alex
|
||
|
||
1. «у фильмов в "Мой список" появились норм названия, годы и у большинства —
|
||
постеры. **Но при открытии все так же "здесь пусто"** и никакого инфо или
|
||
воспроизведения!»
|
||
2. «че блядь значит надо Настройки → Остальное → Основной источник?! мы блядь
|
||
это уже обсуждали и **надо было добавить в CUB список новые секции** а не
|
||
ручное блядь переключение!»
|
||
|
||
### Статус по факту
|
||
|
||
| Пункт | Состояние |
|
||
|---|---|
|
||
| Серверная карточка | ✅ **исправна** — curl отдаёт `tmdb_id: 424781` числом, `source`, жанры, постер, описание |
|
||
| Приёмка на ТВ | ❌ **НЕ пройдена** — «здесь пусто» на экране |
|
||
| Соединения ТВ → Lampac | ❌ **ноль** (`sudo ss -tn \| grep 9118` пусто в момент проверки) |
|
||
| Секции внутри CUB | ❌ не реализовано — сейчас подмена через `e.abort()` |
|
||
|
||
### Диагностика, выполненная в этой сессии
|
||
|
||
Разобран `app.min.js` (2.1 МБ) — **эталон CUB-источника**:
|
||
|
||
- `CUB.full()` ставит `json.source = 'cub'` сам (строка ~34252).
|
||
- `Explorer` читает `this.object.movie || this.object.card`.
|
||
- `full` пушится с `source: element.data.source || 'tmdb'` (**строка 44291**) —
|
||
🔴 **фолбэк на tmdb, если у элемента нет поля `source`**. В ответе
|
||
`/catalog/list` поля `source` **нет** → проверить первым делом.
|
||
- `plugin.js:42-43` требует от карточки: `tmdb_id` (**число**), `original_name`
|
||
(или `original_title`) для определения tv/movie, `img`, `overview`.
|
||
|
||
Полная анатомия — [[personal/tech/lampac-deployment]] → «Анатомия CUB-источника».
|
||
|
||
### 🎯 ЗАДАЧА СЛЕДУЮЩЕЙ СЕССИИ (переформулирована Alex'ом)
|
||
|
||
**Не переключать источник, а дорисовать наши секции ВНУТРЬ CUB.**
|
||
|
||
1. **Проверить гипотезу `source`.** Элементы `/catalog/list` не содержат `source`.
|
||
Если `full` уходит в `tmdb` вместо `watchlist` — это и есть «здесь пусто».
|
||
Проверка: `curl "/catalog/list?plugin=watchlist&cat=films&page=1" | jq '.results[0].source'`.
|
||
2. **Отвязать плагин от перехвата `menu`.** Патчить рендер главной CUB
|
||
(`app.min.js` строки ~44260-44310: `feed`/`category_full`) либо добавить ряды
|
||
через `Lampa.Listener.follow('activity')`. `e.abort()` — НЕ использовать
|
||
(отменяет родной раздел, чего Alex не хочет).
|
||
3. **Приёмка = глазами на ТВ.** Открыть **обычный CUB** → сверху видны наши
|
||
секции с постерами → клик → карточка с описанием → кнопка воспроизведения
|
||
ведёт в TorrServer. Никакого ручного переключения источника.
|
||
|
||
### Питфоллы, найденные в этой сессии
|
||
|
||
- 🔴 **Проверять карточку по `/catalog/card`, НЕ по `/card`.** `/card` отдаёт 404
|
||
и выглядит как поломка каталога — потерян час на ложном диагнозе.
|
||
- 🔴 **Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ** и **переживает
|
||
`docker restart`**. После правки данных старые ответы продолжают отдаваться.
|
||
Проверять на **незэкешированном** `uri` (взять другой фильм).
|
||
- 🔴 **`nodeValue` (`ModInit.cs:285`) возвращает любое `JValue` СТРОКОЙ** →
|
||
в `card_args` нужен `format: "long.TryParse(value, out long v) ? v : 0"`.
|
||
- 🔴 **`tmdb_id` ДОЛЖЕН быть числом** — `plugin.js` делает `if(json.tmdb_id)`.
|
||
- macOS `tar` кладёт AppleDouble `._*` в `wwwroot` → `tar --no-mac-metadata`.
|
||
- Права `root:root 600` → `Permission denied` → статика молча отдаёт **404**.
|
||
Нужно `1000:1000` + `755`/`644`.
|
||
- `find -exec` через `ssh -t` рвётся (`missing argument to -exec`) → `chmod -R`.
|
||
- `&&`-цепочка рвёт tty-сессию ssh → каждая команда отдельным вызовом.
|
||
- `tcpdump` на Кра́кене **не был установлен** → `sudo apt-get install -y tcpdump`.
|
||
- `com.webos.service.devtools` **не существует** на webOS 5.4.2 Lite.
|
||
- `luna-send` пишет в tty → только через `script -q -c "..." /tmp/out.txt`.
|
||
|
||
### Артефакты
|
||
|
||
- Скрипты: `/Users/admin/lampac-tmp/{gen_watchlist_v2.py,deploy_lampac.py,watchlist.yaml}`.
|
||
- Копия UI Lampa для разбора: `/tmp/app-min.js` (2.1 МБ).
|
||
- Бэкап плана: `~/backup-lampac-plugin-plan-20260922-150459.md`.
|
||
- Доклады по карточке — [[personal/tech/lampac-deployment]] (конец файла).
|
||
|
||
## Связанные
|
||
|
||
## ✅ РАБОТАЕТ НА ТВ (2026-09-22, вечер) — подтверждено Alex
|
||
|
||
**«Фильмы из списка Watchlist теперь открываются и проигрываются.»**
|
||
|
||
### 🔴🔴 КОРНЕВАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО» — РУССКИЙ КЛЮЧ ПЛАГИНА
|
||
|
||
Поймано tcpdump'ом на Кра́кене, запрос с ТВ:
|
||
|
||
```
|
||
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 400 Bad Request
|
||
```
|
||
|
||
`plugin=Мой список` — ключ каталога брался из `displayname`.
|
||
|
||
`ModInit.goInit` (строки 51-52):
|
||
|
||
```csharp
|
||
site = site.ToLowerAndTrim();
|
||
site = Regex.Replace(site, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase);
|
||
```
|
||
|
||
**Кириллица вырезается ЦЕЛИКОМ** → `site = ""` →
|
||
`File.Exists("/sites/.yaml")` = false →
|
||
`FindSiteByDisplayName("")` тоже мимо (пустой site не совпадёт с displayname) →
|
||
`return null` → **`init not found`** → HTTP 400 → в Lampa «ЗДЕСЬ ПУСТО».
|
||
|
||
🔴 **Fallback по `displayname` НЕ РАБОТАЕТ с кириллицей** — regex убивает имя
|
||
раньше, чем запускается поиск. Это баг/особенность lampac-nextgen.
|
||
|
||
**ФИКС:** задать `plugin: watchlist` в YAML. Строка `ApiController.cs:136`:
|
||
```csharp
|
||
string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site;
|
||
```
|
||
заданный `plugin` переопределяет ключ каталога → ТВ зовёт `plugin=watchlist` → файл находится.
|
||
|
||
**Приёмка:**
|
||
| Ключ | Ответ |
|
||
|---|---|
|
||
| `plugin=Мой список` | 400 `init not found` |
|
||
| `plugin=watchlist` | 200, `tmdb_id: 424781`, `source: "movie"` |
|
||
|
||
⚠️ **МЕТОДОЛОГИЧЕСКИЙ ВЫВОД (важно):** я проверял карточку своим `curl`'ом с
|
||
`plugin=watchlist` и был уверен, что всё работает. **Надо было ловить РЕАЛЬНЫЙ
|
||
запрос с устройства** (tcpdump на порту сервиса). Урок: при «сервер отвечает 200,
|
||
а в UI пусто» — первым делом снять трафик между устройством и сервером.
|
||
|
||
### Что сделано в этом заходе
|
||
|
||
1. `plugin: watchlist` в `sites/watchlist.yaml` — ключ каталога латиницей.
|
||
2. `card_args` с `format` для числовых полей:
|
||
`tmdb_id` → `long.TryParse`, `runtime` → `long.TryParse`,
|
||
`vote_average` → `double.TryParse`.
|
||
Причина: `ModInit.cs:285` `nodeValue` возвращает ЛЮБОЙ `JValue` как строку, а
|
||
`plugin.js` делает `if(json.tmdb_id)` — нужен настоящий `long`.
|
||
3. `deploy_lampac.py`: проверка ключа `watchlist` (не «Мой список»), проверка карточек
|
||
по `/catalog/card`, заливка `cards/` через tar.
|
||
4. `gen_watchlist_v2.py`: `tmdb_id` числом; дубли ключей
|
||
(`name`+`title`, `original`+`original_title`, `poster`+`img`).
|
||
|
||
## ⏳ ОСТАЛОСЬ ДОБИТЬ
|
||
|
||
1. **Несколько фильмов всё ещё «здесь ничего нет»** — «Рассекая волны», «Хамнет».
|
||
Похоже, для них нет `card-movie-NNN.json` (TMDB не нашёл / Кинопоиск-фолбэк
|
||
без tmdb id) → `initUrl` ведёт на несуществующий файл.
|
||
Лечить: фолбэк-карточка с тем, что есть (название+постер+поиск торрента),
|
||
либо дотягивать tmdb_id по точному названию+году.
|
||
2. **~51 фильм без постера** — TMDB не нашёл ни напрямую, ни через Кинопоиск.
|
||
Разобрать поштучно: возможно, помогают альтернативные названия (`original_title`).
|
||
3. **Секции CUB.** В «Мой список» только 3 секции (Фильмы/Мультфильмы/Аниме),
|
||
«Новинки» куда-то делись? И главное — **фильмов из CUB-фида в списке нет**.
|
||
Требование Alex: **секции должны быть ВНУТРИ CUB**, без ручного переключения
|
||
источника. Механизм: хук `Listener.follow('menu')` в `plugin.js` (строка 59)
|
||
перехватывает `movie`/`cartoon`/`anime`/`tv`/`relise` и делает `e.abort()`,
|
||
НО работает только когда активен наш источник (`Api.getCatalog()` читает
|
||
манифест текущего источника). Нужно доделать так, чтобы при источнике CUB
|
||
наши секции добавлялись к CUB-фиду.
|