Files
obsidian-vault/personal/plans/lampac-watchlist-plugin.md
T

190 KiB
Raw Blame History

created, namespace, related, status, tags, title, type, updated
created namespace related status tags title type updated
2026-09-22 personal
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
main-screen-filter-WORKS-2026-09-25-accepted-latency-prefetch-pending
lampac
lampa
watchlist
catalog
webos
plugin
kraken
radarr
sonarr
torrserver
tmdb
card-parse
feed
matcher-parity
partnext
latency
wlonmain
Watchlist в UI Lampa — каталог из списка plan 2026-09-25

Watchlist в UI Lampa — каталог из списка

🔍 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() в разделе регресса.

// ❌ НЕРАБОЧАЯ ПЕРВАЯ ВЕРСИЯ — оставлена как история ошибки, НЕ КОПИРОВАТЬ
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):

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 его НЕ сбрасывает):

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 — до фикса выглядел так:

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 = инициализация главной → пропустить»:

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. То есть Lampa стоит на главной давно (URL страницы component=main&source=cub), но Activity.active().url пуст — это НЕ стадия инициализации. Пока /catalog не отвечает, __wl_menu пуст и секции взять неоткуда: перезапуск Lampa без починенного /catalog ничего не даст.

Следующий шаг (обязательный, не сделан): проверить боевой запрос Lampa → Lampac /catalog — отвечает ли он с ТВ, отвечает ли с Кра́кена, не сломан ли он вообще.

✅ ИТОГ 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 срабатывает в первом же раунде, секции встают в голову без отставания.

🔴 Коммитить правку НЕКУДА — это выяснено, не пытаться снова

Alex: «Коммить и переходим к предзагрузке». Проверка обеих площадок:

/Users/admin/lampac-tmp        → fatal: not a git repository
/srv/.../lampac  (на Кра́кене)  → fatal: not a git repository

Ни папка сборки, ни папка lampac на Кра́кене под git не лежат. plugin.js — файл, который монтируется в контейнер (./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro), рядом только .bak-копии. То есть версионировать «в лоб» нечего.

Что служит страховкой вместо коммита:

Артефакт Путь Размер
бэкап «до фильтра» catalog/plugin.js.bak-before-mainfilter-20260925-220809 17476 Б
бэкап «до фикса url» catalog/plugin.js.bak-before-fixurl-<ts> 19576 Б
текущий боевой catalog/plugin.js 20934 Б
локальные копии lampac-tmp/plugin-cub.js + build_plugin.py 20934 / 14619 Б

Плюс build_plugin.py держит весь INJECT в себе → файл воспроизводится детерминированно, с 12 проверками инвариантов.

Предложенная развилка (решение за Alex, не сделано): завести в репе watchlist-sync папку lampac_plugin/ и положить туда build_plugin.py + plugin-cub.js под 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() работает, фид виден только на главной. Осталась латентность, см. п.2.
  2. 🔄 ПРЕДЗАГРУЗКА __wl_menu (согласовано Alex: «Коммить и переходим к предзагрузке»). Корень отставания — асинхронный /catalog в loadCatalogs(): меню приходит ПОСЛЕ первого partNext. Решение: подтягивать menu синхронно в startPlugin(), параллельно с loadCatalogs(), чтобы wlInject срабатывал в первом раунде среза. Правка идёт в те же два файла (plugin-cub.js + INJECT в build_plugin.py).
  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_plugin/ в репе watchlist-sync или оставить бэкапы + build_plugin.py (см. «Коммитить НЕКУДА»).

Правило диагностики этой сессии: не читать %{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-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):

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 вставляет секции БЕЗУСЛОВНО:

    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:

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:

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

{"Manifest_global":"undefined","Lampa":"object",
 "Utils":"undefined","network":"undefined"}

Все три — локальные переменные плагина. Первая версия инжекта читала Manifest.catalogs['watchlist'] напрямую → ReferenceError → catch → молча отдавался родной фид. Ошибки нет ни в UI, ни в логах.

❌ Баг 3: Utils.account() без Manifest.api_host подставляет {localhost}.

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 собирает главную:

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.

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)

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:

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 НЕ установлен):

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:

#!/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 —

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

[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 уже записано, деплой не доделан):

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 не отфильтровать.

Диагностика карточки (воспроизводимо):

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

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

Команды (порядок):

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 (доказательство, воспроизводимо):

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.

# 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 (то, что нужно было брать с самого начала):

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

if (!url.StartsWith("http"))
    url = $"{init.host}/{url}";     // ListController.cs:87

host из YAML сканируется по HTTP (Http.Get / Playwright / RCH). Локальные файлы напрямую Catalog читать не умеет — только HTTP-ответ.

Два режима парсинга

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)

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 правки

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

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

Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; } });

Свойство создаётся не-enumerable → Object.keys() его не видит → Lampa не рисует источник в панели выбора. При этом Api.sources['Мой список'] доступен напрямую и каталог отдаёт.

Фикс (одна правка, два вхождения):

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

# docker-compose.yml
volumes:
  - ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
# 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).

Готовые команды диагностики ТВ (переиспользовать)

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

Откат

# выключить каталог
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) после патча.

Управление

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

Откат

# выключить каталог
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

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:

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)

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)

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:

// строка 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.

Фикс (одна перестановка):

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 должны дать свои числа, а не общий список):

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

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

⚠️ Проверено обе формы — обе ломают:

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

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

"menu": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... },
"main": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... }

Писать main в YAML не нужно и нельзя. Достаточно menu.

Правильная форма 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:
    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):

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. 🔴 ВОТ ИСТОЧНИК СКАЧКИ:

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

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

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, воспроизводимо)

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

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 (замер):

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:

ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq .
{
  "BitTorr": {
    "CacheSize": 100663296,        // 96 МБ — RAM-кэш
    "UseDisk": false,              // 🔴 НА ДИСК НЕ ПИШЕТ
    "TorrentsSavePath": "",        // путь сохранения пуст
    "TorrentDisconnectTimeout": 180,  // раздача живёт 3 мин после остановки
    "PreloadCache": 0,
    "ReaderReadAHead": 83
  }
}

UseDisk: false — TorrServer держит всё в RAM (96 МБ) и на диск не пишет ничего. Это ровно то, что нужно Alex. Настраивать не надо, уже так.

TorrServer жив и работает (проверено)

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.

# что прописывает 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:

# ⚠️ работает БЕЗ префикса /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 ✅

Вывод: поиск торрентов работает штатно, ничего поднимать не надо.

Кто качает на диск — полный реестр (единственные виновники)

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

# ключ: /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 — настройки (замер)

ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.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 (подтверждено)

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)

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

    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 — бэкап (обязательно, до всего)

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

curl -sL -X DELETE "http://localhost:7878/api/v3/queue/<id>?removeFromClient=true&blocklist=false&apikey=$KEY"

⚠️ Не использовать blocklist=true — иначе релизы попадут в чёрный список и не найдутся при ручной загрузке. Файлы на диске не трогаются.

Шаг 2 — снять monitored со всех фильмов

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

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 — проверить

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, чтобы не пересоздавать контейнер дважды.

Откат

# вернуть 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.

Связанные

✅ РАБОТАЕТ (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 пишем оба набора ключей.

Воспроизведение

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

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 как строку.

    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#, возвращает настоящий тип)

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.

Эталонный рабочий ответ

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

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:

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-фиду.