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

229 KiB
Raw Blame History

title, created, updated, type, namespace, tags, related
title created updated type namespace tags related
Lampac — развёртывание и эксплуатация (Кра́кен) 2026-09-22 2026-09-25 tech personal
lampac
torrserver
docker
kraken
streaming
webos
personal/plans/lampac-kraken
personal/plans/lampac-watchlist-plugin
personal/plans/watchlist-tmdb-missing-marker
family/how-to/lampac-torrents
personal/tech/lampac-torrserver
family/how-to/arr-stack-kraken
personal/tech/multistream-single-flow-limit
family/how-to/kraken-network
family/how-to/tv-luna-send
personal/tech/prowlarr-flaresolverr-cloudflare

Lampac — развёртывание и эксплуатация (Кра́кен)

Технический справочник. Развёрнуто 2026-09-22. План и статус шагов — personal/plans/lampac-kraken.

🔴 2026-09-25: деплой plugin.js модуля Catalog — контракт и подводные камни

Правки фида Watchlist в UI Lampa живут НЕ в YAML, а в plugin.js модуля Catalog (патч enumerable:true + инжект секций через перехват Lampa.Api.partNext). Полный план и статус — personal/plans/lampac-watchlist-plugin.

Где лежит файл — ✅ ИСПРАВЛЕНО 2026-09-25: ЭТО РЕПОЗИТОРИЙ, а не /Users/admin/lampac-tmp/

🔴 /Users/admin/lampac-tmp/ — рабочая папка сессии 2026-09-22, НЕ под git. Живое место исходников — репозиторий ~/Developer/lampac-scripts (Mac), залитый в Gitea git_admin/lampac-scripts. Именно туда кладутся build_plugin.py и plugin-cub.js; lampac-tmp — временный скрэтч, из него файлы могут исчезнуть.

Слой Путь Примечание
Mac, репо ~/Developer/lampac-scripts/scripts/build_plugin.py сборщик, 12 инвариантов
Mac, репо ~/Developer/lampac-scripts/scripts/plugin-cub.js 🔴 собранный выход, РУКАМИ не правим
Mac, скрэтч /Users/admin/lampac-tmp/ рабочая папка, вне git — не источник истины
Кра́кен, хост /srv/dev-disk-by-uuid-6194539b-.../lampac/catalog/plugin.js bind-mount, вне git
В контейнере /lampac/module/Catalog/plugin.js :ro по compose
По HTTP http://192.168.1.14:9118/catalog.js что реально грузит ТВ

Репозиторий: https://git.mallexxx.duckdns.org/git_admin/lampac-scripts.git (remote origin, ветка main). Локально в нём же: scripts/gen_watchlist.py, scripts/deploy_lampac.py, scripts/mark_missing.py, tests/. Коммит плагина 2026-09-25: 5e55a47 «секции CUB только на главной экране».

cd ~/Developer/lampac-scripts
git fetch origin && git status --short --branch     # держим синхрон с Gitea
git add scripts/build_plugin.py scripts/plugin-cub.js
git commit -m "..." && git push origin main

⚠️ В lampac/ на Кра́кене git НЕТ (проверено: fatal: not a git repository). Репы на Кра́кене только три: docker/, obsidian/, watchlist-sync/. Бэкапы боевого файла — .bak-<что>-<дата> рядом с plugin.js, они закрыты .gitignore (*.bak*).

🔴 wwwroot/catalog.js НЕ существует. wwwroot/ содержит только JSON фида (watchlist.json, watchlist-<cat>.json, cards.json, cards/, missing.json). Попытка scp оттуда падает с «No such file or directory» — это не сбой сети.

Порядок деплоя

коммит ДО (репо) → правка build_plugin.py → сборка → заливка → ПРОВЕРКА АЛЕКСОМ → коммит ПОСЛЕ
cd ~/Developer/lampac-scripts/scripts
python3 build_plugin.py          # инварианты: сборка падает при нарушении
node --check plugin-cub.js       # SYNTAX OK обязателен
# бэкап боевого
ssh kraken 'cp .../lampac/catalog/plugin.js .../catalog/plugin.js.bak-<что>-$(date +%Y%m%d-%H%M%S)'
scp plugin-cub.js kraken:.../lampac/catalog/plugin.js
# проверка, что файл виден в контейнере (bind-mount)
ssh kraken 'docker exec lampac grep -c "<маркер>" /lampac/module/Catalog/plugin.js'

🔴 plugin-cub.js в репе — это ВЫХОД сборщика. Правку вносим в INJECT внутри build_plugin.py, потом пересобираем; руками plugin-cub.js не редактируем, иначе следующая сборка затрёт правку. Инварианты в checks{} обязаны покрывать любую новую функцию — иначе регресс пройдёт молча.

🔴 Перезапуск Lampa на ТВ обязателен

docker restart lampac НЕ сбрасывает кэш catalog.js в браузере webOS — ТВ продолжит исполнять старую версию плагина. Нужен рестарт приложения на ТВ:

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'

⚠️ luna-send через ssh 'script -q -c "luna-send ... -a {\"id\":...}"' ломается на экранировании JSON (luna-send печатает help) — поэтому скрипт кладётся файлом на ТВ.

🔴 Форма Lampa.Activity.active() — ИЗМЕРЕНО на живом ТВ 2026-09-25

Lampa.Activity.active()          → {url, title, component, source, page, params, activity}
Lampa.Activity.active().activity → {component, object, slide, body, is_stopped, is_started}
                                  ↑ url ЗДЕСЬ НЕТ

url лежит на ВЕРХНЕМ уровне. Обращение к .activity.url всегда даёт undefined — на этом была потеряна приёмка 2026-09-25 (фильтр «только главная» смотрел не туда и секции исчезли полностью). Читать оба уровня, верхний первым.

🔴 CDP-диагностика ТВ (порт 9998) — питфоллы

curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl'

Клиент — websocket-client (модуля websockets на Mac НЕТ). Готовый скрипт: /Users/admin/lampac-tmp/wl_console.py.

  • JSON.stringify(Lampa.Activity.active()) → TypeError: Converting circular structure to JSON. Возвращать поля по одному.
  • Обращение к отсутствующему полю в Runtime.evaluate без обёртки → ответ без result.value, выглядит как «все пробы вернули <none>». Оборачивать в String(...) / typeof ..., проверять subtype == "error".
  • Runtime.enable + Log.enable дают 0 строк, если Lampa уже загрузилась: сообщения, напечатанные до подключения, не отдаются. Логи читать сразу после рестарта приложения либо подтверждать состояние пробами.
  • 🔴 URL страницы ТВ — надёжный индикатор экрана: ...index.html?title=Главная%20-%20CUB &component=main&source=cub = главная CUB.
  • 🔴🔴 НЕ ВЕРИТЬ ПЕРВОМУ ЗАМЕРУ ПОСЛЕ РЕСТАРТА LAMPA. Сразу после luna-send проба даёт ready=false, plugin=false, patch=false, act_url=ERR, cats=NOMANIFEST — выглядит как «плагин вообще не доехал» (window.plugin_catalog ставит сам startPlugin()). Это гонка, а не поломка: через несколько секунд те же пробы дают ready=true, plugin=true, patch=true. При загрузке CDP-цель имеет title="" и url=about:blank. Правило: искать цель по подстроке url = com.lampac.tv, а НЕ по title == "Lampac" (в момент загрузки title пустой, и скрипт молча не находит цель — выглядит как «ТВ недоступен»). Опрашивать ready, пока не станет true. 🔴 И не спать вслепую: Alex на sleep 20 без проверки отреагировал резко («Чё нахуй за слип 20?!»). Опрашивать состояние в цикле, не ждать таймером.

🔴 /catalog — контрольная проверка бэкенда (норма 2026-09-25)

Прежде чем чинить что-либо в UI, убедиться, что эндпоинт жив:

ssh kraken 'curl -s -o /dev/null -w "http=%{http_code} size=%{size_download} t=%{time_total}\n" \
  http://127.0.0.1:9118/catalog'                    # → http=200 size=21470 t=0.44s (холодный)
ssh kraken 'curl -s -o /dev/null -w "http=%{http_code}\n" http://192.168.1.14:9118/catalog'  # → 200 по LAN

Тело содержит watchlist.menu = {Новинки, Фильмы, Мультфильмы, Аниме}. Если здесь всё исправно — причина в клиенте (гонка загрузки), а не в бэкенде.

🔴 Manifest.catalogs пуст = фид вставлять некуда

Если /catalog от Lampac не ответил, loadCatalogs() не отработал → window.__wl_menu не создан → секции Watchlist не появятся ни на одном экране, независимо от фильтров. Наблюдалось 2026-09-25: catalog_all = NOMANIFEST, wl_menu = undefined, при этом patch = true. Диагностировать это ПЕРВЫМ, до разбора логики фильтра.

⚠️ Manifest.catalogs и window.__wl_menu расходятся по времени. Наблюдалось menu = Новинки,Фильмы,Мультфильмы,Аниме (заполнен) при cats = NOMANIFEST (пуст) — __wl_menu пишется в wlMenu() отдельным запросом. Не считать пустой catalogs доказательством, что меню нет: смотреть оба поля.

🔴 2026-09-24 (вечер): «Мастерская сатаны» — один источник, торренты пусты. План диагностики

Вопрос Alex: «для Мастерская сатаны например в lampac в источниках есть только один результат через свой lampa сервер в ужасном качестве, в торрентах по нему ничего не нашлось в lampac, надо проверить почему, по каким трекерам он ищет, и можем ли мы влиять на списки источников например через prowlarr резолвер?»

Что установлено фактами (проверено в этой сессии)

1. /lite/pidtor по этому фильму отдаёт 503 из КЭША, а не «0 раздач».

GET http://192.168.1.14:9118/lite/pidtor?title=Мастерская сатаны&year=2025
  → HTTP 503, Content-Length: 0, X-Invoke-Cache: HIT, t=0.007–0.035s

Тот же симптом, что чинили 2026-09-22: кэш extend: 2700 (45 мин) переживает docker restart lampac. ⚠️ Пока кэш не снят, «в торрентах ничего не нашлось» — не вывод о трекерах вообще. Пусто будет по любому заголовку.

🔴 Ошибка при разборе 22-го: «кэш в cache/fdb/» — НЕВЕРНО. Фактически cache/fdb/ содержит только пустые каталоги (0/ 1/ 2/ 3/ 4/ 5/ 7/ 9/ a/ b/ d/ e/ f/, 0 файлов во всех). Реальный кэш лежит в $H/lampac/cache/module/ — 76 файлов без расширений с base64-ish именами (OEdMn4LsmVEkyTEYgfu5dg), это сборки .NET-модулей (PE32 Mono/.Net DLL), а не JSON-ответы. Нашёл по содержимому: grep -l "pidtor" $H/lampac/cache/module/* → PidTor.dll (lite/pidtor, lite/pidtor/serial/{id}, lite/pidtor/s{id}).

⚠️ Вывод для будущих чисток: перед удалением cache/fdb/ проверить, что там вообще есть файлы. Пустая папка = чистка ничего не даст; кэш модулей лежит в cache/module/, и трогать его надо с оглядкой (модули пересобираются).

2. По каким трекерам ищет PidTor: свой список он НЕ держит — ходит на внешний jac.red.

Lampa (TV) → «Торренты» → PidTor (модуль lampac)
                             ↓ redapi
                        jac.red (ВНЕШНИЙ, 144.31.3.10)
                             ↓ /api/v2.0/indexers/all/results?title=..&year=..
                        «all» = ВСЕ индексеры JacRed сразу
                             ↓ MagnetUri + info{}
                        TorrServer (встроен, порт 9085 внутр. → /ts/ на 9118)

🔴 Prowlarr к PidTor отношения не имеет. У Prowlarr свои 6 индексера (RuTracker · Kinozal · Kinozal (M) · RuTor · NoNaMe Club · Byrutor), и он питает Radarr/Sonarr, не PidTor. Прямого моста «Prowlarr → источники Lampa» нет.

3. Влияние на список источников — два реальных пути, оба не «через Prowlarr»:

Путь Как Блокер
локальный JacRed lampac править набор трекеров в его БД ($H/lampac/data/) и переключить redapi на http://127.0.0.1:9118 🔴 локальный JacRed отдаёт формат БЕЗ поля info → PidTor/Controller.cs:97 torrent.info.sizeName → NPE → 500. Переключать только после того, как локальный начнёт отдавать info
Prowlarr в обход PidTor добавить трекеры в Prowlarr и забирать ими напрямую (в Radarr/Sonarr) это не «источники в Lampa»; в плеере их не будет

4. Один источник в «Смотреть онлайн» и «ужасное качество» — это про онлайн-балансеры, не про PidTor. «Смотреть онлайн» обслуживают Online/OnlineRUS/OnlinePaid/SISI (20 активных балансеров из /lite/events), поток идёт с чужого CDN, реклама 1xbet рисуется обёрткой источника. Качество диктует источник, а не lampac.

План диагностики (записан, НЕ выполнен — ждёт апрува Alex)

  1. Снять 503: docker stop lampac → очистить cache/fdb/ (предварительно проверив, что там есть файлы) → docker start lampac → повторить /lite/pidtor по «Мастерская сатаны». ~5 мин.
  2. Замерить сырой jac.red: GET http://jac.red/api/v2.0/indexers/all/results?title=<Название>&year=<Год> — сколько результатов и по каким Tracker (jq '[.Results[].Tracker]|group_by(.)|map({(.[0]):length})|add'). 5 мин.
  3. Развести два исхода: 0 результатов → смотреть конфиг индексеров jac.red (только чтение); результаты есть, но PidTor режет → проверить min_sid: 15 и совпадение года. 5–10 мин.
  4. Отдельно: описать в Obsidian, через какой механизм реально расширять список источников.

⚠️ Шаг 1 требует docker stop lampac — зона Alex (docker-оп над контейнерами он делает сам). ⚠️ Шаги 2–3 (curl к внешнему jac.red) в этой сессии ушли в таймаут апрува (BLOCKED ×2) — не повторять без явного согласия.

Связано: family/how-to/lampac-torrents · personal/tech/lampac-torrserver · family/how-to/arr-stack-kraken · personal/tech/prowlarr-flaresolverr-cloudflare

🔴 2026-09-24: генератор каталога признан вторым матчером — уходит в проект

lampac/scripts/gen_watchlist.py (878 строк) + mark_missing.py (179) — отдельные скрипты вне watchlist-sync. Разбор кода в этой сессии:

  • резолв трёхуровневый: Radarr по названию → TMDB /search/{movie,tv} (кэш cache/.lampac_tmdb_cache.json) → КП по номеру из ссылки в .md (kp_id_from_url, приоритет выше поиска по названию) → КП по названию с порогом KP_SIM_THRESHOLD = 0.75 и проверкой год ±1 / совпадения media_type;
  • детали карточек — cache/.lampac_tmdb_details.json (описание, жанры, актёры, runtime, ru-title + year; требование Alex «отображать как в tmdb/kp»);
  • пишет: wwwroot/watchlist.json (channels), wwwroot/watchlist-{films,new,cartoons,anime}.json, wwwroot/cards.json + wwwroot/cards/card-<type>-<id>.json (файл на фильм, обёртка {"card": {...}}), wwwroot/missing.json (для маркера ❓);
  • позиции без TMDB-ID в фид не отдаются (требование «чего нет для просмотра — того нет в списке»), уходят в missing.json;
  • load_env() ищет .env в жёстком списке путей (Кракен-пути + Mac-путь) — хрупко.

Проблема: скрипт не читает .resolve_cache.json и не знает маркеров ✅/🗓️/❓ — это ВТОРОЙ независимый матчер того же списка. Ранее связывался с resolve неявно через Radarr (матч по названию → те же версии, что выбрал резолвер). После удаления контента из arr (2026-09-24) мост исчез → 3 карточки сменили фильм.

Решение (✅ РЕАЛИЗОВАНО 2026-09-24, коммит a524f5d): логика переехала НЕ в отдельный feed.py, а внутрь sync_down.py — Alex уточнил: «это же чистая доработка sync-down?». sync-down сам пишет фид (wwwroot/watchlist.json, watchlist-<cat>.json, cards/, cards.json, missing.json) + двусторонний ❓. Пути: --feed-root → env LAMPAC_ROOT → <repo>/../lampac; UUID-путей в коде нет. arr.py удалён, ⬇️ не ставится.

🔴 Блокер, вскрытый переносом: резолвер проекта матчит хуже старого генератора — 86 позиций получают другой tmdb_id. Причина: в gen_watchlist.py есть ступень «КП-по-номеру из ссылки kinopoisk.ru/film/N/», в resolver.py её нет. Поэтому gen_watchlist.py / mark_missing.py / watchlist-catalog.sh (крон 02:30) пока НЕ удалены — из них берётся код kp_search_by_id. Детали, три открытия при переносе и порядок шагов — personal/plans/lampac-watchlist-plugin § «2026-09-24 (вечер): ПЕРЕПИСЫВАНИЕ».

⚠️ НЕ трогать при переносе: catalog/sites/watchlist.yaml (plugin: watchlist — обязателен, кириллица в ключе вырезается регуляркой ModInit; первая категория игнорирует format и берёт list.uri; card_args требуют format для числовых типов, иначе tmdb_id приедет строкой и карточка в Lampa будет «здесь пусто»), catalog/plugin.js (инжект секций в CUB через Api.partNext), структуру cards/.

Что это

lampac-nextgen/lampac (MIT, .NET 10) — серверный бэкенд для Lampa. Один контейнер, порт 9118.

Внутри (всё в одном образе):

  • TorrServer — subprocess, кэш раздач
  • JacRed — Jackett-совместимый индексатор
  • LampaWeb — веб-морда
  • TmdbProxy, DLNA, Sync

🔴 TorrServer встроенный. Отдельный контейнер не нужен. Проверено 2026-09-22: в логах первого старта видно compilation TorrServer. Внешний TS — лишняя сущность.

Расположение на Кра́кене

/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/
  ├── docker-compose.yml          (бэкапы .bak-*, .bak2-*)
  ├── config/       → /lampac/config
  ├── cache/        → /lampac/cache   (кэш TorrServer — только сюда)
  ├── data/         → /lampac/data    (БД JacRed)
  ├── config-module/init.conf     → /lampac/init.conf:ro   (наш конфиг)
  ├── catalog/sites/              → /lampac/module/Catalog/sites
  ├── catalog/override/           → /lampac/module/Catalog/override
  ├── catalog/plugin.js           → /lampac/module/Catalog/plugin.js:ro  (ПАТЧ, см. ниже)
  ├── wwwroot/                    → /lampac/wwwroot  (JSON-каталог watchlist + cards/)
  └── scripts/gen_watchlist.py    (генератор .md → JSON)

Локальные копии рабочих артефактов (на Mac, /Users/admin/lampac-tmp/): gen_watchlist_v2.py, patch_plugin.py, plugin-patched.js, watchlist.yaml, docker-compose.yml, deploy_lampac.py, build_plugin.py (сборщик патча секций), plugin-cub.js (результат), deploy_cub.py, tv_restart.sh, restart_lampa.sh, probe_*.py (CDP-пробы рантайма Lampa на ТВ).

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

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

🔴 2026-09-25: тем же правилом туда переехал плагин каталога — scripts/build_plugin.py + scripts/plugin-cub.js (коммит 5e55a47). До этого оба жили только в lampac-tmp. lampac-tmp НЕ источник истины: его содержимое может исчезнуть, исходники плагина — только в репозитории.

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

/Users/admin/Developer/lampac-scripts/
  ├── scripts/gen_watchlist.py      (из gen_watchlist_v2.py — ПРОД-генератор)
  ├── scripts/deploy_lampac.py
  ├── scripts/mark_missing.py       (маркер ❓, вход — missing.json)
  ├── scripts/build_plugin.py       (сборщик плагина каталога)     ← 2026-09-25
  ├── scripts/plugin-cub.js         (собранный выход, не править руками) ← 2026-09-25
  └── tests/accept_step1.py, tests/accept_step1_order.py

Первый коммит: 1765c05 (gitea git_admin/lampac-scripts, private). Проверка push: git rev-parse HEAD == git ls-remote origin refs/heads/main.

Второй коммит: 0345df2 — фиксы деплоя (.env-путь, clear_failed_cache()), переделанный mark_missing.py, watchlist-catalog.sh впервые в git.

🔴 watchlist-catalog.sh до этого коммита жил ТОЛЬКО на Кра́кене (/home/kraken/scripts/), в git его не было — потерялся бы вместе с SD. Правило: любой скрипт, который вызывает крон на боевой машине, обязан быть в репозитории. Проверять git status не только в репо, но и по факту: «залил scp-шкой» ≠ «закоммитил». В этой сессии коммит откладывался трижды, и каждый раз это всплывало как вопрос Alex.

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

🔴 Автозакачка в Radarr ОТКЛЮЧЕНА (2026-09-22)

Alex: «ТЕБЕ БЛЯДЬ ЕЩЁ СУКА НЕ ОЧЕВИДНО ЧТО БЛЯДЬ ЗАКАЧЕК БЫТЬ НЕ ДОЛЖНО СКОТИНА?!»

Что было: watchlist-nightly.sh:25-27 вызывал watchlist-sync sync-down --apply --batch 2, а sync_down.py (radarr_add() + radarr_search(), строки 155/158) добавлял фильмы в Radarr и триггерил поиск раздач — закачки уходили без спроса, до 2 за прогон.

Что стало: вызов sync-down вырезан из cron-скрипта. Крон теперь:

sync-vault → process-thumbs → resolve --recheck --apply → sync-up → sync-vault

Бэкап прежней версии: watchlist-nightly.sh.bak-20260922-182655. Сумма новой: 4401882e3b1307a73b3330a70b59ecfe.

🔴 sync_down.py как КОД оставлен — запускается вручную, если понадобится. Отключён именно автоматический прогон по крону, а не возможность. Возврат — только по прямому указанию Alex.

Единственный автоматический путь в закачку (проверено grep по ~/scripts/*.sh): в активном виде не осталось ни одного. Все упоминания sync-down — комментарии.

⚠️ Radarr уже получил закачки за прошлые прогоны — 16 фильмов, УДАЛЕНЫ 2026-09-22 вместе с файлами (см. раздел «Инвентаризация авто-закачек» ниже).

Разграничение двух «лимитов» (частый источник путаницы)

Лимит Что ограничивает Где
batch=2 сколько позиций уходит в Radarr на закачку за прогон sync-down, был в крон
лимита нет выдача в Lampa — отдаётся весь watchlist.json wwwroot/

Каталог строится .md → watchlist.json; Radarr в отображении не участвует. Alex спрашивал про «лимит обработки/выдачи в lampa» — это НЕ batch.

✅ Инвентаризация авто-закачек Radarr/Sonarr и УДАЛЕНИЕ (2026-09-22)

Alex: «удалить все добавленные автоматикой закачки в т.ч. файлы. и radarr и sonarr»

Как определялось «добавлено автоматикой»

У Radarr/Sonarr нет тега «кто добавил». Признак — окно работы sync-down: пайплайн пошёл в работу 20–21.09.2026.

RADARR:  50 фильмов. added >= 2026-09-20  ->  16 штук  ← это авто
         34 более старых (18.05–04.06)    ->  НЕ авто (watchlist тогда не работал)
SONARR:  13 сериалов, ВСЕ добавлены 18.05–01.06  ->  авто-закачек НЕТ (added>=20.09: 0)

Удалено: 16 фильмов Radarr вместе с файлами

deleteFiles=true&addImportExclusion=false
DELETE /api/v3/movie/{id}   -> все 16 вернули HTTP 200, ошибок 0
id Фильм Год Файл
40 American History X 1998 был
41 Oficina do Diabo 2025 нет
42 I Swear 2025 был
43 Gladiator 2000 нет
44 Warfare 2025 был
45 The Secret Agent 2025 был
46 Den of Thieves 2: Pantera 2025 был
47 The Ugly Stepsister 2025 был
48 Rental Family 2025 был
49 The Life List 2025 был
50 People We Meet on Vacation 2026 был
51 It Was Just an Accident 2025 был
52 The Perfect Neighbor 2025 нет
53 Chasing the Wind 2025 нет
54 Sirāt 2025 был
55 Death of a Unicorn 2025 был

Результат (проверено сырьём):

Radarr:  50 -> 34 фильма   |   added >= 20.09: 0   |   очередь: 0   |   monitored: 0
все 12 каталогов с файлами: удалены с диска (movie-radarr: 29 -> 17 каталогов)
корзина .recyclebin: пуста (файлы ушли физически, не в корзину)
Sonarr:  13 сериалов, не тронуто (авто-закачек не было)

Артефакты: дамп /tmp/radarr-sonarr-dump-20260922-184351/, снимок /tmp/radarr-delete-snapshot.json, скрипт ~/lampac-tmp/delete_autodownloads.py.

Повторное добавление невозможно — два независимых барьера: monitored: 0 у всех 50 + sync-down вырезан из крона (см. выше).

🔴🔴 ПИТФОЛЛ: /media/movies-radarr существует в ДВУХ разных местах

Это стоило ошибочного вывода «файлы не удалились».

СНАРУЖИ хоста:   /media/movies-radarr                     -> SD-ПУСТЫШКА (0 файлов!)
                                                    это /dev/mmcblk0p2, корень SD
ВНУТРИ Radarr:   /srv/dev-disk-by-uuid-49e8f586-…/media/movies-radarr  -> РЕАЛЬНЫЕ файлы
                 (17 каталогов)   это ОТДЕЛЬНЫЙ диск 1.8T

Radarr монтирует ДРУГОЙ диск, не тот, где живёт lampac:

uuid 49e8f586-3839-4c5d-a1e1-58bfc3579ade  → медиа (movies-radarr, series-sonarr, downloads)
uuid 6194539b-03e0-4f33-986b-4da09834e82f  → lampac, docker, obsidian
Оба — sda1 1.8T, но это РАЗНЫЕ разделы/диски

Правильная проверка маунтов:

docker inspect radarr --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
# /srv/dev-disk-by-uuid-49e8f586-…/media            -> /media
# /srv/dev-disk-by-uuid-49e8f586-…/media/downloads  -> /downloads
# /srv/dev-disk-by-uuid-6194539b-…/docker/radarr/config -> /config

🔴 find по наружному пути ВРЁТ — он смотрит на SD-пустышку. Всегда сверяться с docker inspect маунтами.

🔴 ПИТФОЛЛ: setsid nohup НЕ спасает фоновый процесс от обрыва ssh

Прогон генератора каталога умирал дважды на client_loop: send disconnect: Broken pipe (на 100/366 позиций). setsid nohup ... & не помогает — ssh забирает группу процессов при разрыве.

Рабочее решение — systemd-run:

systemd-run --user --unit=gen-watchlist --scope /tmp/gen-runner.sh
# Running scope as unit: gen-watchlist.scope

Процесс отвязан от сессии, доживает до конца даже при обрыве ssh.

🔴 ПИТФОЛЛ: проверять состояние, а не полагаться на «удалил»

После удаления первой проверкой я смотрел /media/movies-radarr (SD-пустышку), увидел 0 файлов и решил, что всё удалено — случайно угадал. Правильная проверка — по реальному маунту 49e8f586, там movie-radarr: 29 -> 17.

🔴 Кэш резолва переехал с SD-карты на HDD (2026-09-22)

Alex: «какого хуя что-то лежит на sd карте блядь?!»

Было: ~/.lampac_tmdb_cache.json (72 КБ) и ~/.lampac_tmdb_details.json (426 КБ) в домашнем каталоге kraken — на SD-карте (/dev/mmcblk0p2, 14 ГБ). SD медленная, изнашивается, теряется при переустановке/апдейте системы.

Стало: пути выводятся из расположения самого скрипта:

def _cache_dir() -> Path:
    side = Path(__file__).resolve().parent.parent / "cache"
    if side.is_dir():
        return side          # Кра́кен: <lampac>/cache (HDD)
    return Path.home()       # fallback для запуска с Мака

CACHE_FILE   = _cache_dir() / ".lampac_tmdb_cache.json"
DETAILS_FILE = _cache_dir() / ".lampac_tmdb_details.json"

Патч: ~/lampac-tmp/patch_hdd_cache.py, бэкап .bak-hddcache.

⚠️ Файлы скрытые — имя начинается с точки. Проверять ls -la или find, а не ls <dir>/*.json: глоб * их НЕ находит. На этом я ошибся дважды и сначала решил, что копирование не сработало.

Проверено по времени правки файла: кэш на HDD обновляется прогоном, на SD застыл. Копия на SD удалена — см. следующий раздел.

✅ Копия кэша с SD УДАЛЕНА (2026-09-22, 18:42)

удалено с SD:  /home/kraken/.lampac_tmdb_cache.json     (72 118 Б, было 18:24)
               /home/kraken/.lampac_tmdb_details.json  (425 929 Б, было 18:24)
бэкап:         /tmp/sd-cache-backup-20260922/  (оба файла, до удаления)
HDD на месте:  $H/lampac/cache/.lampac_tmdb_{cache,details}.json  (18:29, свежие)

Проверка, что на SD не пересоздаются (verify_cache_path.py):

CACHE_FILE   → $H/lampac/cache/.lampac_tmdb_cache.json      существует: True
DETAILS_FILE → $H/lampac/cache/.lampac_tmdb_details.json    существует: True
_cache_dir() → /srv/.../lampac/cache   это HDD? True
SD-путь /home/kraken/.lampac_tmdb_cache.json  существует: False

_cache_dir() строит путь от __file__, поэтому Path.home() больше не выбирается при запуске на Кра́кене — генератор физически не может писать на SD.

📋 Инвентаризация авто-закачек в Radarr/Sonarr (2026-09-22)

✅ ВЫПОЛНЕНО: 16 фильмов Radarr удалены вместе с файлами (см. ниже «Удаление»). Sonarr не тронут — авто-закачек там не было.

Полный дамп: /tmp/radarr-sonarr-dump-20260922-184351/ (radarr-movies.json, sonarr-series.json) — единственный откат, Radarr корзины не имеет. Снимок состояния перед удалением: /tmp/radarr-delete-snapshot.json.

Доступ к API (креды из /srv/...6194539b.../watchlist-sync/.env)

UR=http://localhost:7878/radarr   # Radarr :7878, сегмент /radarr
US=http://localhost:8989/sonarr   # Sonarr :8989, сегмент /sonarr
curl -sL "$UR/api/v3/movie"      -H "X-Api-Key: $RADARR_KEY"
curl -sL "$US/api/v3/series"     -H "X-Api-Key: $SONARR_KEY"
curl -sL "$UR/api/v3/queue"      -H "X-Api-Key: $RADARR_KEY"   # очередь загрузок
curl -sL "$UR/api/v3/wanted/missing?pageSize=50" -H "X-Api-Key: $RADARR_KEY"

🔴 Только curl -sL (без -L редирект даёт 301 → пустой ответ). Рабочий скрипт: ~/lampac-tmp/dump_arr.sh, обёртка dump_arr_wrap.sh.

Radarr: 50 фильмов, из них 16 добавлено автоматикой

Признак авто-закачки — added >= 2026-09-20 (окно работы sync-down; пайплайн watchlist'а включён в сентябре). Всё добавленное 18.05–04.06 — ручное.

added id Фильм Файл на диске
2026-09-20 40 American History X (1998) ✅
2026-09-20 41 Oficina do Diabo (2025) ❌
2026-09-20 42 I Swear (2025) ✅
2026-09-20 43 Gladiator (2000) ❌
2026-09-20 44 Warfare (2025) ✅
2026-09-20 45 The Secret Agent (2025) ✅
2026-09-20 46 Den of Thieves 2: Pantera (2025) ✅
2026-09-20 47 The Ugly Stepsister (2025) ✅
2026-09-20 48 Rental Family (2025) ✅
2026-09-20 49 The Life List (2025) ✅
2026-09-20 50 People We Meet on Vacation (2026) ✅
2026-09-20 51 It Was Just an Accident (2025) ✅
2026-09-20 52 The Perfect Neighbor (2025) ❌
2026-09-20 53 Chasing the Wind (2025) ❌
2026-09-20 54 Sirāt (2025) ✅
2026-09-21 55 Death of a Unicorn (2025) ✅

12 с файлами, 4 без. Root folder Radarr: /media/movies-radarr (свободно 511 GB — чистка не требовалась). Теги: [] (пусто).

Sonarr: авто-закачек НЕТ

13 сериалов, все добавлены 18.05–01.06, ни одного в окне 20–21.09: Our Flag Means Death, Stranger Things, The Cyberfarm, Magical Police Department, Место встречи изменить нельзя, MOZU, Delicious in Dungeon, Rick and Morty, Severance, Brassic, Double or Quits, Futurama, True Detective. Root: /media/series-sonarr.

Гарантия «заново не добавятся» — два независимых барьера

1. sync-down вырезан из watchlist-nightly.sh   → в кроне вызова нет
2. все 50 фильмов Radarr: monitored=false      → поиск раздач не запускается
   + очередь (queue) 0 записей
   + wanted/missing 0 записей

✅ УДАЛЕНИЕ ВЫПОЛНЕНО (2026-09-22, 18:44) — 16/16, файлы с диска ушли

Скрипт: ~/lampac-tmp/delete_autodownloads.py (Python + urllib, не bash: кириллица/$() в ssh-командах рвётся). Список ID фиксирован в коде, dry-run печатает план, --apply удаляет.

🔴 Radarr по умолчанию НЕ удаляет файл с диска. Нужен явный deleteFiles=true в DELETE-запросе, иначе файл останется в /media/movies-radarr/<Title> (<Year>)/ сиротой. Корзины нет — удаление необратимо без /tmp-дампа.

url = "%s/api/v3/movie/%d?deleteFiles=true&addImportExclusion=false" % (URL, i)
req = urllib.request.Request(url, method="DELETE")
req.add_header("X-Api-Key", KEY)

Результат: удалено: 16, ошибок: 0 (все HTTP 200).

Radarr:  50 → 34 фильма     B added >= 20.09: 0
очередь: 0                  wanted/missing: 0
monitored: 0                .recyclebin: пусто (файлы ушли физически)
12 каталогов в $M/movies-radarr исчезли; осталось 17
Sonarr:  13 сериалов, added >= 20.09: 0 (авто-закачек не было, не трогали)

Снимок до удаления: /tmp/radarr-delete-snapshot.json. Скрипт проверки: ~/lampac-tmp/verify_deleted.sh, ~/lampac-tmp/verify_final.sh.

🔴🔴 КРИТИЧНО: /media/movies-radarr существует в ДВУХ разных местах

Первый find дал ложный результат «каталогов: 0» — я смотрел не туда.

снаружи:        /media/movies-radarr                    → SD-пустышка, 0 файлов,
                                                          принадлежит root
в контейнере:   /srv/…49e8f586…/media/movies-radarr     → РЕАЛЬНЫЙ диск, 29 → 17 каталогов

docker inspect radarr --format '{{range .Mounts}}...':

/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media          -> /media
/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media/downloads -> /downloads
/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker/radarr/config -> /config

🔴 Radarr монтирует ДРУГОЙ диск (49e8f586), не тот, где lampac (6194539b). Оба — sda1 1.8T, но это разные маунты/файловые пространства. Проверять только через маунты контейнера, а не по наружному пути. $M = /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media.

Структура реального медиа-диска: cartoons/, cartoons-series/, documentaries/, documentaries-series/, downloads/{complete,incomplete,radarr,sonarr}/, movies/, movies-radarr/, music/, series/, series-sonarr/, .recyclebin/.

⚠️ Удаление 12 фильмов с файлами освободило место в $M/movies-radarr, но не влияет на каталог Lampa — watchlist.json строится из .md, Radarr в отображении не участвует.

🔴 Маркер ❓ работает В ОБЕ СТОРОНЫ (2026-09-22, финал)

Симптом: .md расходился с каталогом

Три правки Alex (Духless, Бумбараш, Everything Everywhere All at Once) были в watchlist.json с постерами, но в .md стояли с ❓. Казалось, резолв не работает — на деле резолв прошёл, а маркер остался с прошлого прогона.

Первопричина

mark_missing.py реализовывал правило «позиции НЕТ в missing.json → не трогать». Маркер ставился, но никогда не снимался. Правка названия → резолв проходит → ❓ в .md остаётся навсегда.

Фикс

Правило заменено на «нет в missing.json → СНЯТЬ ❓». Список нерезолвленных — ИСТИНА, работает в обе стороны.

# mark_missing.py, analyze()
if norm(title) not in missing:
    stats["не_в_missing"] += 1
    if QUESTION in body:                       # <- НОВОЕ: снятие
        cleaned = re.sub(r"\s*" + QUESTION + r"\s*", " ", body).strip()
        stats["снято"] += 1
        changes.append((i, raw.rstrip("\n"),
                        "%s- [%s] %s" % (indent, box, cleaned)))
    continue

⚠️ Защита «файл стал меньше → отмена» удалена — снятие метки законно уменьшает файл. Замена: число строк-позиций (ITEM_RE) не должно измениться.

Боевой прогон

всего 596 | не в missing 350 | календарь 39 | просмотрено 179 | уже вопрос 10
снято 24 | поставлено 18
размер 29632 -> 29608 Б (-24) | позиций 596 -> 596
вопрос 34 -> 28

Коммит lampac-scripts 1122b71 (0345df2..1122b71 main -> main).

🔴 Питфолл: два формата ключа кэша — не путать сырьё с резолвом

'{title}|'           <- ключ tmdb_search()      -> list[3] = РЕЗОЛВ ОТРАБОТАЛ
'kp|{title}|{limit}' <- ключ kp_search()        -> list[5] = СЫРЬЁ поиска КП
'kpid|{id}'          <- ключ kp_search_by_id()

Наличие '{title}|' = список [tmdb_id, media_type, poster] = резолв успешен. А 'kp|...' со списком = документы Кинопоиска, резолва может и не быть. Я принял второе за первое и раздул ложную тревогу по Джеймс Кемерон морские приключения? — фильма с таким названием не существует, ❓ на нём правильный.

🔴 Питфолл: grep -c считает СТРОКИ, а не вхождения

grep -c дал 34 — я счёл это числом маркеров. Правильный счёт — str.count() в Python по прочитанному файлу. Проверять сырьём, не грепом по счётчикам.

🔴 Питфолл: кириллица в grep через ssh = блок апрува

ssh kraken 'grep "Духless" file' → запрос апрува → таймаут → BLOCKED. Рабочий обход — скрипт файлом: write_file локально → scp → ssh bash /tmp/x.sh.

🔴 Питфолл: структура watchlist.json

Ключ названия — name, НЕ title. Проверка «есть ли фильм в каталоге» обязана читать channels.*.items[].name, иначе даёт ложный «0 позиций».

{ "generated": true, "count": 373,
  "channels": { "films": { "title": "Фильмы", "items": [
      { "name": "...", "original": "...", "year": null, "rating": 0.0,
        "image": "https://image.tmdb.org/...", "href": "..." } ] } } }

Путь боевого .md

$H/obsidian/family/documents/movies-watchlist.md ($H = /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac), kraken:kraken, бэкапы /tmp/movies-watchlist.md.bak-* + .bak-questions рядом.

✅ Название и год карточки — ИЗ TMDB, не из .md (2026-09-22)

Требование Alex (дословно): «че по имени в карточке? оно должно из источника ставиться.» + ранее «БЛЯДЬ ... Ничего не укорачивать. Отображать как в tmdb/kp.»

Что было не так

Генератор писал it["name"] = название из .md, it["year"] = год из .md. Проверено сырьём — TMDB отдаёт другое:

в .md:             TMDB ru-RU:            TMDB en-US:
'Смерть единорога' → 'Смерть Единорога'    'Death of a Unicorn'   ← регистр!
'Everything…'      → 'Всё везде и сразу'   'Everything…'          ← русское
'Духless'          → 'Духless'             'Soulless'

Плюс year вообще приходил null, хотя TMDB знает release_date, а в кэше деталей не было поля с названием — только cast/country/genres/overview/ poster_path/runtime/tagline/vote.

Фикс: tmdb_details() тянет имя + год

url = (f"https://api.themoviedb.org/3/{mtype}/{tmdb_id}"
       f"?api_key={api_key}&language=ru-RU&append_to_response=credits")
ru_title   = d.get("title") or d.get("name") or ""
orig_title = d.get("original_title") or d.get("original_name") or ""
date       = d.get("release_date") or d.get("first_air_date") or ""
year       = int(date[:4]) if re.match(r"^\d{4}", date) else None
out = {..., "title": ru_title, "original_title": orig_title,
       "en_title": en_title, "year": year, "release_date": date}

В enrich() — приоритет источник → .md как fallback:

src_name = (d.get("title") or "").strip()
if src_name:
    it["name"] = src_name          # русское название TMDB
src_orig = (d.get("original_title") or "").strip()
if src_orig:
    it["original"] = src_orig      # оригинал
if it.get("year") is None and d.get("year"):
    it["year"] = d["year"]         # год из release_date

⚠️ Год из .md не перетирается, если он там был — он может быть точнее для локальных премьер РФ.

🔴 Миграция старого кэша деталей

367 записей были без поля title. Добавлена проверка в начале tmdb_details(): если у записи нет title — перезапрашивается из TMDB (остальные 8 полей не теряются, запись заменяется целиком).

if key in details:
    d = details[key]
    if d.get("title"):     # уже мигрировано
        return d
    # иначе падаем дальше в запрос

Боевой прогон (EXIT=0)

376 позиций | без года: 1 (из 376) | 296 с русским названием + оригиналом

'Смерть единорога'  → 'Смерть Единорога'    orig='Death of a Unicorn'  year=2025
'Everything…'       → 'Всё везде и сразу'    orig='Everything…'         year=2022
'Простите за бесп…' → чисто                  orig='Sorry to Bother You' year=2018
'Духless'           → 'Духless'              orig='Духless'             year=2012
'Бумбараш'          → 'Бумбараш'             orig='Бумбараш'            year=1972
'Я ругаюсь'         → orig='I Swear' · 'Гамнет' → orig='Hamnet'
'Марти Великолепный'→ orig='Marty Supreme'

Единственная позиция без года: Волшебный участок: Фильм — в TMDB год не проставлен.

Коммит lampac-scripts 53e5803 (запушен: 1122b71..53e5803 main -> main).

🔴 Питфолл прогона: setsid nohup НЕ спасает от обрыва ssh

Прогон умирал дважды на client_loop: send disconnect: Broken pipe — ssh-сессия забирала группу процессов с собой. Рабочее решение:

systemd-run --user --unit=gen-watchlist --scope /tmp/gen-runner.sh
# Running scope as unit: gen-watchlist.scope   ← отвязан от сессии

Обёртка ~/lampac-tmp/run_gen_systemd.sh пишет лог в /tmp/gen-run.log и добавляет EXIT=$? в конец. Проверка: pgrep -f gen_watchlist.py.

⚠️ Путь к .md — на уровень ВЫШЕ lampac: $H/obsidian/family/documents/movies-watchlist.md, где $H = /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f. Ошибочно подставлял $H/lampac/obsidian/... → [ERR] нет источника.

🔴🔴 Источники воспроизведения: торренты ≠ «смотреть онлайн» (2026-09-22)

Вопрос Alex: «у меня есть определенные сомнения что через torrserver идут, там только пункт "смотреть онлайн" работает и он добавляет вотермарк и рекламу 1xbet — это точно с торрентов? потому что если пункт "торренты" выбрать при просмотре — то там пусто.»

Ответ: сомнения подтвердились. Это НЕ торренты.

Два независимых механизма воспроизведения в Lampa:

Пункт в UI За ним Откуда поток Реклама
«Торренты» модуль TorrServer (порт 9085) раздача через P2P НЕТ
«Смотреть онлайн» Online + OnlineRUS + OnlinePaid + SISI чужой CDN источника ДА: 1xbet

🔴 Реклама 1xbet приходит от ОНЛАЙН-ИСТОЧНИКА, а не от TorrServer. TorrServer отдаёт поток как есть, баннеры рисует обёртка онлайн-источника.

Точные пути и механизм

/lampac/module/TorrServer       ← торренты (жив: /echo -> "MatriX.135")
/lampac/module/Online           ← базовый модуль онлайн-источников
/lampac/module/OnlineRUS        ← 23 русских источника
/lampac/module/OnlinePaid       ← Rezka и платные
/lampac/module/OnlineUKR | OnlineENG | OnlineAnime | OnlineGEO
/lampac/module/SISI             ← 18+ (не источник рекламы для обычных фильмов)

/lite/events (НЕ /api/, НЕ /online) отдаёт список активных балансеров — 20 штук, формат {"name":..,"url":"/lite/<x>","balanser":"<x>"}:

Filmix · pizdatoehd · Kinobase · Phantom · Kinogo · AniDub · LeProduction ·
VeoVeo · PidTor · Collaps · HDVB · Rutube · VK Видео · Kinoteatr.kg ·
Vibix · Kinotochka · Eneyida · UaKino · UAFilm · Geosaitebi

/lampac/module/OnlineRUS/ = 23 каталога: CDNvideohub · Collaps · FanCDN · FlixCDN · Gencit · HDVB · Kinobase · Kinogo · Kinoteatrkg · Kinotochka · LeProduction · Mirage · Phantom · PizdatoeHD · RuDub · RutubeMovie · Spectre · VeoVeo · Vibix · VideoDB · Videoseed · VkMovie · Zetflix · ZetflixDB

Почему «Торренты» пусто — ✅ РАЗОБРАНО ПОЛНОСТЬЮ И ПОЧИНЕНО (2026-09-22)

Вывод: причина — redapi, а не TorrServer. ИСПРАВЛЕНО, ПРИНЯТО В UI.

ПРИЧИНА  → PidTor.redapi указывал на ЛОКАЛЬНЫЙ JacRed lampac,
           который отдаёт формат БЕЗ поля info
           → PidTor/Controller.cs:97 torrent.info.sizeName → NPE → 500 → кэш 503
ФИКС     → redapi возвращён на http://jac.red (отдаёт полный формат)
ПРИЁМКА  → ✅ Alex в UI: «Во! теперь появились торренты. запустил F1..»

Первая версия объяснения («нет раздач — нет сидов») была неполной. Полная диагностика ниже доказала, что раздачи находятся. Отдельный док: personal/tech/lampac-torrserver, разбор фикса: family/how-to/lampac-torrents.

TorrServer жив и качает

docker exec lampac sh -c 'curl -s http://127.0.0.1:9085/echo'   # -> MatriX.135

Тест живым магнитом (Big Buck Bunny) — раздача добавилась и пошла:

{"title":"probe","name":"Big Buck Bunny","stat":3,
 "stat_string":"Torrent working","total_peers":343,"active_peers":7,
 "connected_seeders":5,"bytes_written":6918,"bytes_read":49072}

⚠️ Тестовая раздача удалена после проверки: POST /ts/torrents {"action":"rem","hash":"<hash>"} → HTTP 200.

Торрент-поиск работает по всей цепочке

JacRed  /api/v1.0/torrents?search=Матрица   → HTTP 200, 74 КБ, magnet ✅
        трекеры: kinozal, torrentby, rutor, rutracker
PidTor  /lite/pidtor?title=Матрица&year=1999 → HTTP 200, 8435 Б, раздача ✅
jac.red → 144.31.3.10, HTTP 200 ✅
/lite/events → "Pid<s>T</s>or" присутствует ✅

🔴 Почему я поначалу решил, что TorrServer сломан — питфолл маршрутов

TorrServer НЕ доступен по порту 9085 для клиента. Lampac проксирует его под префиксом /ts/ на порт 9118:

http://127.0.0.1:9118/ts/echo        -> MatriX.135
http://127.0.0.1:9118/ts/{*suffix}   -> прокси на TorrServer API
http://127.0.0.1:9118/ts/stream      -> воспроизведение
http://127.0.0.1:9118/ts/playlist    -> плейлист
http://127.0.0.1:9118/ts/download/   -> скачивание

Мои первые запросы GET http://127.0.0.1:9085/torrents → 404, и я принял это за поломку. Снаружи контейнера порт 9085 вообще даёт Connection refused. Проверять только через docker exec lampac либо через /ts/ на 9118.

🔴 Метод запроса ВАЖЕН — и 400 вместо 404 = маршрут существует:

POST /ts/torrents  -d '{"action":"list"}'  -> 200, []   ✅ ТОЛЬКО ТАК
POST /ts/torrents  с телом []              -> 400
POST /ts/torrents  без тела                -> 400
POST /ts/torrents  с телом {}              -> 400
GET  /ts/torrents                          -> 404  (метода нет)
GET  /ts/torrents/list                     -> 404
GET  /ts/settings                          -> 403  (нужна авторизация ts:tspass)

🔴 Исправлено 2026-09-22: ранняя запись говорила, что [] даёт список. Это неверно — проверено перебором: [], {} и пустое тело → 400. Рабочий вызов ровно один:

curl -s -X POST http://127.0.0.1:9118/ts/torrents \
  -H 'Content-Type: application/json' -d '{"action":"list"}'

⚠️ Раздачи живут в памяти процесса TorrServer — docker restart lampac их выгружает (тестовая probe исчезла сама). Файл data/ts/ при этом не трогается.

🔴🔴 РЕАЛЬНАЯ причина пустого списка у Alex: redapi (найдено 2026-09-22)

Не min_sid, не TorrServer. Причина — redapi в секции PidTor:

"PidTor": { "enable": true, "enabled": false,
            "redapi": "http://jac.red",     ← ДОЛЖНО БЫТЬ ИМЕННО ТАК
            "min_sid": 15, "torrs": null, "auth_torrs": null }

Если redapi переключить на локальный JacRed lampac (http://127.0.0.1:9118), PidTor падает на всех фильмах: локальный JacRed отдаёт формат БЕЗ поля info, а PidTor/Controller.cs:97 читает torrent.info.sizeName → NPE → HTTP 500.

НАШ JacRed  → поля без "info"
jac.red     → + info{quality, voices, sizeName, name, originalname, relased}
PidTor ждёт → torrent.info.sizeName

Проверка: curl -s -o /dev/null -w '%{http_code}' \ "http://127.0.0.1:9118/lite/pidtor?title=Гамнет&year=2025" → должно быть 200.

Полный разбор: family/how-to/lampac-torrents

⚠️ Вторичный фильтр — min_sid: 15 (сток). Сиды < 15 отсекаются; свежие фильмы часто имеют 1–37. Понижение — решение Alex (низкий sid = медленнее).

⚠️ README PidTor устарел: там написано «пункт появляется только если заданы torrs/auth_torrs, иначе список пустой». Фактически при null /lite/pidtor отдаёт рабочие раздачи. Верить факту, не README.

⚠️ enabled: false — НЕ выключатель. Это поле стоит false у всех 86 модулей (Filmix, HDVB, Kinobase, PidTor…). Смотреть только enable.

vast.js — НЕ виновник (проверено сырьём)

$H/wwwroot/vast.js          = библиотека VASTPlayer (25×ads, 18×VAST, 6×AdTag)
$H/wwwroot/sisi/index.html  → vast.js НЕ подключён
$H/wwwroot/sisi/plugins/modification.js (22 КБ):
    реклама/трекеры (1xbet|vast|adTag|preroll): 0 совпадений
    домены: только w3.org (XML-неймспейсы)
    "torrent": 1 упоминание (сообщение «нет файлов»)

modification.js — чистый клиент TorrServer'а: берёт element.json, зовёт Api$1.account(element.video + '&json=true'), играет качества. Рекламы в нём нет.

🔧 Выключатель: LampaWeb.initPlugins.online в init.conf

/lampac/base.conf (из образа):

"LampaWeb": { "initPlugins": {
    "jacred": true, "tmdbProxy": true, "cubProxy": true,
    "online": true,       // ← «смотреть онлайн»: 20 балансеров + реклама
    "sisi": true,         // ← 18+ модуль
    "torrserver": true,   // ← торренты (не трогать)
    "timecode": true
}}

Пользовательский override уже смонтирован в контейнер (:ro): $H/config-module/init.conf → /lampac/init.conf. Сейчас там только catalog: true. Он перекрывает base.conf.

Чтобы погасить рекламу — добавить "online": false в initPlugins.

⚠️ Цена решения (объяснить Alex до правки):

• Реклама 1xbet пропадёт
• Останется ТОЛЬКО «Торренты». Нет раздач → смотреть нечего
• Онлайн-просмотр (Rezka/Filmix/Rutube/VK) исчезнет ВООБЩЕ

План и варианты: personal/plans/lampac-disable-1xbet-ads.

🔴🔴 СТАТУС: НЕ ПРИМЕНЕНО И НЕ БУДЕТ. Alex: «Я БЛЯДЬ НЕ ПРОСИЛ ГАСИТЬ ОНЛАЙН, ЕБЛАН! Я СКАЗАЛ ЧИНИТЬ torrserver!» Задача была — диагностировать TorrServer, а не гасить онлайн. План откачен, init.conf не менялся. Док оставлен как справка, где лежит выключатель; применять только по прямому указанию Alex.

📄 Текущее состояние $H/config-module/init.conf (2026-09-22, вечер)

Это единственный меняемый конфиг lampac. Полное содержимое:

{
  "BaseModule": {
    "SkipModules": ["Tracks","Transcoding","WebLog","CacheMedia",
                    "ForkPlayerXML","MsxNative","Potok","TelegramAuth",
                    "TelegramAuthBot"]
  },
  "LampaWeb": { "initPlugins": { "catalog": true } },
  "PidTor": { "enable": true, "redapi": "http://jac.red" }
}

Что здесь важно:

Ключ Значение Почему
LampaWeb.initPlugins.catalog true включает модуль Catalog — без него «Мой список» не виден
PidTor.enable true модуль торрентов активен
PidTor.redapi http://jac.red 🔴 НЕ менять на локальный — см. выше, ломает PidTor

⚠️ online: false отсутствует намеренно — Alex запретил гасить онлайн.

Бэкапы (2 шт., оба на HDD рядом с конфигом):

$H/config-module/init.conf.bak-pidtor-20260922-191505
$H/config-module/init.conf.bak-20260922-112154   (в $H/, от прошлой правки)

Порядок безопасной правки (правило Alex «коммит ДО → правка → заливка → проверка»):

H=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac
cp $H/config-module/init.conf $H/config-module/init.conf.bak-$(date +%Y%m%d-%H%M%S)
# правим локально, scp обратно
python3 -c "import json; json.load(open('$H/config-module/init.conf')); print('JSON ok')"
docker restart lampac

⚠️ Применить конфиг без рестарта нельзя. Рестарт = ~35 сек недоступности плеера. Раздачи в TorrServer при рестарте сохраняются (data/ts/ не трогается).

🚀 Деплой генератора на Кра́кен (проверено 2026-09-22)

🔴🔴 Питфолл: load_env() искал .env только по Mac-пути

# БЫЛО — генератор на Кра́кене не находил ключей вообще
for p in (Path("/Users/admin/Developer/watchlist-sync/.env"),):

Симптом на Кра́кене: [ERR] нет TMDB_KEY в .env → каталог собирался на одних Кинопоиск-ссылках, без TMDB-ID и постеров. Никакой резолв не выполнялся, хотя код для него был. Именно это объясняет, почему в JSON не было TMDB-ID у половины позиций — не «слабый резолв», а его полное отсутствие.

СТАЛО — перебор 5 мест, печать прочитанных файлов в stderr:

candidates = [
    os.environ.get("WATCHLIST_ENV", ""),
    "/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync/.env",
    "/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/watchlist-sync/.env",
    str(Path.home() / "Developer" / "watchlist-sync" / ".env"),
    str(Path.home() / ".config" / "watchlist-sync" / ".env"),
]

Патч: ~/lampac-tmp/patch_envpath.py. Урок: при деплое скрипта на другую машину — первым делом проверять абсолютные пути к конфигам, а не логику.

Установка: только sudo install, не cp

scp -q ~/Developer/lampac-scripts/scripts/gen_watchlist.py kraken:/tmp/gen_watchlist.py
ssh kraken "sudo install -o kraken -g kraken -m 600 /tmp/gen_watchlist.py \
            $H/scripts/gen_watchlist.py"

$H = /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac

🔴 Права — источник немого провала. $H/scripts/ = kraken:kraken, но ~/scripts/ = root:root 755. Простой cp в ~/scripts/ падает на Permission denied (бэкап крона-скрипта так и не сделался с первого раза). Скрипты генератора — 600 kraken:kraken; крон-обёртки в ~/scripts/ — 755 root:root.

Проверка после заливки — три уровня, все обязательны:

md5 -q  <локальный файл>                                  # сверка с Кра́кеном
ssh kraken "md5sum $H/scripts/gen_watchlist.py"           # суммы должны совпасть
ssh kraken "python3 -m py_compile $H/scripts/gen_watchlist.py && echo OK"
ssh kraken "bash -n ~/scripts/watchlist-catalog.sh && echo OK"

Что задеплоено (2026-09-22)

Файл Путь Права md5
генератор $H/scripts/gen_watchlist.py kraken:kraken 600 3c7d4df34ee20f84b92c1b4be46d8382
маркер $H/scripts/mark_missing.py kraken:kraken 600 43b3bb15e2aaad1ccb352d705990406c (до фикса снятия)
крон-скрипт ~/scripts/watchlist-catalog.sh root:root 755 17672d62285de5f33e0a89776ba34416

🔴 mark_missing.py перезалит в ночь 2026-09-22 с фиксом «маркер в обе стороны» (снятие ❓). Прежний md5 43b3bb15… устарел — сверять по python3 mark_missing.py на dry-run: должен показывать строку снято в статистике. Если её нет — залита старая версия, маркер сниматься не будет.

Бэкап прежнего генератора: $H/scripts/gen_watchlist.py.bak-20260922-175754.

Боевой прогон (эталон для сравнения)

ssh kraken "cd $H && python3 $H/scripts/gen_watchlist.py \
    /home/kraken/obsidian/family/documents/movies-watchlist.md \
    $H/wwwroot/watchlist.json"
Метрика Значение
позиций 373
без TMDB-ID 0
films / new / cartoons / anime 331 / 32 / 3 / 7
постеров 327 / 32 / 3 / 7
missing.json 30
источники Radarr 9 · TMDB 343 · автопоиск 2b 17 · 2c 4 · miss 30

watchlist.json 102 456 Б · missing.json 1 239 Б · карточек в cards/ — 373.

🔎 Резолв позиций в каталоге — три уровня (2026-09-22)

Генератор ищет фильм по цепочке, от точного к эвристике:

1)  Radarr по названию
2)  TMDB по названию как есть
2c) Кинопоиск по KP_ID ИЗ ССЫЛКИ .md -> имя/EN-алиас -> TMDB   ← машинный ключ
2b) Кинопоиск по НАЗВАНИЮ           -> имя/EN-алиас -> TMDB   ← эвристика
3)  miss -> позиция в JSON каталога НЕ попадает, имя в missing.json -> ❓ в .md

🔴 2c ВЫШЕ 2b намеренно. kp_id из ссылки — машинный ключ, он не зависит от переименований. Пример: в .md «Астрал. Могильный ритуал 2», КП по id 11825624 отдаёт актуальное имя «Могильный ритуал. Зло вернулось» → TMDB 1576953. Если бы 2b шёл первым, он привязал бы 1-ю часть (1264397) по общему префиксу.

🔴 KP_SIM_THRESHOLD = 0.75 — порог близости документа КП к запросу. Документ с sim ≥ 0.75, но нерезолвенный в TMDB = окончательный промах, дальше перебирать нельзя. Без порога перебор уходил в мусор: по «WeWork» релевантный документ (sim=1.00) не резолвился из-за латинской C в слове «Cоздание» (дефект данных КП), и скрипт брал We Work for the Dead (1135465) вместо 795622.

⚠️ Порог и kp_id — числовые/машинные критерии, не ручной словарь соответствий. Инвариант «никаких ручных правок» не нарушен.

🔴 Кэш промахов — генератор чистит его САМ

~/.lampac_tmdb_cache.json (на Кра́кене) кэширует и успехи, и промахи (None).

Проблема: промах писался навсегда. Если Alex правит название так, что ключ кэша не меняется (регистр, пробел, порядок слов) — запрос в сеть не уходит, возвращается старый None, и ❓ не снимается никогда. Плюс копятся None по давно не используемым названиям.

Решение — clear_failed_cache(cache) в генераторе, вызывается в main() сразу после load_cache():

cleared = clear_failed_cache(cache)   # удаляет все None
if cleared:
    print(f"[i] кэш: снято промахов {cleared} (будут перепроверены)", file=sys.stderr)
  • Успешные резолвы (числовой TMDB-ID) не трогаются — они верны, а повторная проверка ~350 записей на каждом прогоне бессмысленна.
  • Промахи (~50) перезапрашиваются всегда — это ровно те позиции, ради которых правки и делаются. Стоимость: секунды.

⚠️ Ручная чистка больше не нужна. До этого фикса приходилось гонять clear_cache_kraken.py руками — теперь это часть прогона.

Патч: ~/lampac-tmp/patch_autoclear_cache.py. Бэкап: .bak-cacheclear. Сумма после заливки: c556c52b8cf4714686add4083c4afc09.

missing.json и маркер ❓

Генератор пишет снимок нерезолвленных рядом с каталогом:

{"generated": true, "count": 30, "missing": ["All systems red", "..."]}

Маркер ставится по факту промаха, а не по тексту .md:

python3 scripts/mark_missing.py --missing <wwwroot>/missing.json <путь.md> [--apply]

🔴 Первая версия mark_missing.py решала «нет TMDB» по тексту строки и дала 344 пометки вместо 18 (помечала рабочие позиции с постерами). В прод не пошла. Сопоставление теперь по нормализованному названию (нижний регистр, ё→е, без пунктуации/эмодзи, без хвостового года). 🗓️ и [x] не трогаются.

🔴🔴 Маркер ❓ работает В ОБЕ СТОРОНЫ (фикс 2026-09-22, ночь)

Симптом: Духless, Бумбараш, Everything Everywhere All at Once были в watchlist.json с постерами и TMDB-ID, но в боевом .md стояли с ❓. Расхождение файла и каталога.

Первопричина: правило было «позиции НЕТ в missing.json → не трогать». mark_missing.py только ставил ❓ и никогда не снимал. Правка названия Alex'ом → резолв проходит → маркер остаётся навсегда.

Фикс: нет в missing.json → снять ❓, если он есть. Список нерезолвленных — ИСТИНА, работает в обе стороны.

# mark_missing.py, analyze()
if norm(title) not in missing:
    stats["не_в_missing"] += 1
    if QUESTION in body:                       # ← НОВОЕ: снятие
        cleaned = re.sub(r"\s*" + QUESTION + r"\s*", " ", body).strip()
        changes.append((i, raw.rstrip("\n"),
                        "%s- [%s] %s" % (indent, box, cleaned)))
    continue

⚠️ Защита «файл стал меньше → отмена» УДАЛЕНА — она стала ложной: снятие маркера законно уменьшает файл (2 байта на позицию). Замена — число строк-позиций (ITEM_RE) не должно измениться.

Dry-run на боевом .md (596 позиций): снять 24, поставить 18, уже_вопрос 10, 🗓️ 39, [x] 179, не в missing 350.

🔴 Два формата ключа кэша — как проверять факт резолва

'{title}|'           ← tmdb_search()   -> list[3] = [tmdb_id, media_type, poster]
'kp|{title}|{limit}' ← kp_search()     -> list[5] = документы КП
'kpid|{id}'          ← kp_search_by_id()

472 ключа: None (промах) — 59 · list — 408 · dict — 5. Ключ '{title}|' со списком = резолв УСПЕШЕН. Самый быстрый способ проверить, найдётся ли позиция, не гоняя генератор.

🔴 Проверка «есть ли фильм в каталоге» — ключ поля name, НЕ title

{ "count": 373,
  "channels": { "films": { "title": "Фильмы",
      "items": [ { "name": "...", "image": "...", "href": "..." } ] } } }

Проверка через .title даёт ложный «0 позиций». Читать channels.*.items[].name. Эталон: 373 позиции · 369 с постерами · 373 с href.

🔴 Диагностика на Кра́кене: только скриптом ФАЙЛОМ

ssh kraken 'grep "Духless" file' → блок апрува → таймаут → BLOCKED. Кириллица в аргументах инлайн-команд через ssh не проходит (см. family/how-to/tv-luna-send.md — тот же класс проблемы).

Рабочий шаблон:

# 1. write_file локально -> /Users/admin/lampac-tmp/probe_x.py (или .sh)
# 2. scp /Users/admin/lampac-tmp/probe_x.py kraken:/tmp/probe_x.py
# 3. ssh kraken 'bash /tmp/probe_x.sh'   # или python3 /tmp/probe_x.py

⚠️ Импорт генератора в пробнике: sys.path.insert(0, "$H/scripts"), затем import gen_watchlist as G и обязательно env = G.load_env() — свой упрощённый разбор .env даёт KP=NO TMDB=NO и ложный вывод «ключей нет».

Боевой путь .md и бэкапы

$H/obsidian/family/documents/movies-watchlist.md    # 29 632 Б, kraken:kraken

$H = /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac. Бэкапы: /tmp/movies-watchlist.md.bak-live, .bak-run2, .bak-questions (от маркера). 🔴 find / -name "movies-watchlist.md" без -maxdepth возвращает пусто на этом хосте — искать по диапазонам -maxdepth 5..6.

🔴 grep -c считает СТРОКИ, а не вхождения

grep -c '❓' дал 34, и это было принято за число маркеров. Правильно — str.count() в Python по прочитанному файлу. Для счётчиков символов всегда Python, не греп.

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

По watchlist заданий ПЯТЬ, а не одно (проверено crontab -l, 2026-09-22):

0 1  * * *   watchlist-nightly.sh     process-thumbs → resolve → sync-down → sync-up
0 9  * * 0   watchlist-discover.sh    новинки из KP (воскресенье)
0 2  * * *   watchlist-cleanup.sh     сносит просмотренное при диске >90%
30 2 * * *   watchlist-catalog.sh     JSON-каталог для Lampa
0 *  * * *   sync-vault.sh            синк vault (не watchlist, но связан)

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

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

🔴 Разделение ролей: кто пишет в .md, кто отправляет в закачку

Скрипт Что делает Закачка
discover (вс 09:00) только пишет в .md — добавляет новинки ❌ ничего не отправляет
sync-down (в nightly) шлёт в Radarr: radarr_add() + radarr_search() (sync_down.py:155,158) ✅ отправляет, но через Radarr, не напрямую в torrent
resolve --recheck резолвит позиции, снимает маркер сам, когда фильм найден ❌
cleanup (02:00) удаляет файлы просмотренного при заполнении диска ❌
catalog (02:30) генерация JSON + mark_missing + docker restart lampac ❌

🔴 Лимит закачки ЕСТЬ. --batch 2 — снижен с 5 после инцидента 2026-09-21 (диск 100%). Плюс проверка свободного места _free_gb() в sync_down.py. «Проходит весь список без лимитов» — неверно, идёт по 2 позиции за прогон.

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

🔴 Права на файлы в wwwroot/ — владелец 1000:1000 (= пользователь lampac в контейнере), каталоги 755, файлы 644. Не kraken:kraken — контейнер ходит не под хост-пользователем. Если файл окажется root:root 600, dotnet ответит Permission denied, а веб-сервер молча отдаст 404 — отсутствие прав выглядит снаружи как «файла нет».

Раздача статики из wwwroot/

  • Всё, что положено в /lampac/wwwroot/, доступно по корню: wwwroot/watchlist-films.json → GET /watchlist-films.json (200).
  • UseStaticFiles — в Core.dll (серверное ядро), настраивать не нужно.
  • 🔴 /lampac/wwwroot ≠ /lampac/current.conf: статика ядра, а не модуль. Проверять наличие файла надо docker exec lampac ls ..., а доступность — обязательно через curl, потому что это разные вещи (см. права выше).
  • ⚠️ Веб-корень отдаёт только wwwroot/. Пути вида /cards/... работают, но /card — это не статика (см. ниже).

⚠️ Всё пишущее — на data-диск (1.8T). НЕ на SD (/, 14G, 8.6G свободно).

docker-compose.yml (актуальный, 2026-09-22)

services:
  lampac:
    image: ghcr.io/lampac-nextgen/lampac:latest
    container_name: lampac
    platform: linux/arm64
    restart: unless-stopped
    ports:
      - "9118:9118"
    environment:
      - TZ=Asia/Bishkek
      - LANG=ru_RU.UTF-8
    volumes:
      - ./config:/lampac/config
      - ./cache:/lampac/cache
      - ./data:/lampac/data
      # Наш конфиг: перекрывает base.conf из образа. Включает модуль Catalog.
      # Без монтирования правки теряются при пересоздании контейнера.
      - ./config-module/init.conf:/lampac/init.conf:ro
      # Каталоги Catalog
      - ./catalog/sites:/lampac/module/Catalog/sites
      - ./catalog/override:/lampac/module/Catalog/override
      # Патч plugin.js. Файл живёт В ОБРАЗЕ — без монтирования патч теряется
      # при обновлении образа. Нужен для видимости источника в UI (см. ниже).
      - ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
      # Статика: JSON-каталог watchlist (Lampac отдаёт wwwroot по HTTP сам)
      - ./wwwroot:/lampac/wwwroot

⚠️ В контейнере рабочий каталог /lampac, часть файлов читается из корня, не из config/.

Минимальный config-module/init.conf

Перекрывает base.conf, поэтому список SkipModules нужно повторить целиком, без Catalog:

{
  "BaseModule": {
    "SkipModules": [
      "Tracks", "Transcoding", "WebLog", "CacheMedia", "ForkPlayerXML",
      "MsxNative", "Potok", "TelegramAuth", "TelegramAuthBot"
    ]
  },
  "LampaWeb": { "initPlugins": { "catalog": true } }
}

После любой правки — docker compose up -d (пересоздаст контейнер) или docker restart lampac.

Модуль Catalog — внутренняя механика (разобрано по коду 2026-09-22)

Исходники внутри образа: /lampac/module/Catalog/ — Controllers/{ApiController,ListController,CardController}.cs, ModInit.cs, plugin.js, sites/*.yaml.

Маршруты

Роут Назначение
/catalog.js плагин для Lampa (клиентский JS)
/catalog JSON: список каналов с их menu и main
/catalog/list?plugin=<p>&cat=<c>&page=N список элементов категории
/catalog/card?plugin=<p>&uri=<id>&type=movie карточка фильма

WAF на префиксе /catalog/ — 10 запросов/сек.

⚠️ Открытый вопрос: displayindex не поднимает «Мой список» выше встроенных блоков

Замечено Alex 2026-09-22: каталог watchlist в ленте идёт после блоков «Сейчас Смотрят» и «Рекомендуем посмотреть». Это не задумано.

watchlist.yaml: displayindex: 1 — самый низкий из всех каталогов:

displayindex Каталог
1 watchlist («Мой список»)
10 Rezka
15 Kinobase
20 Filmix, FilmixApp
25 KinoPub
30 VoKino
35 Eneyida
40 KinoUkr
45 GetsTV
60 AniLiberty

«Сейчас Смотрят» и «Рекомендуем посмотреть» — встроенные каталоги Lampa, их нет в catalog/sites/, поэтому displayindex их не сдвигает: Lampa держит свои блоки отдельным списком.

Что проверять при разборе: доступен ли приоритет встроенных блоков в init.conf/настройках Lampa, или встроенные всегда идут первыми. Понижать displayindex ниже 1 бессмысленно — он уже минимальный.

🔴 Питфолл №1: первая категория игнорирует format

ListController.cs:50 строит базовый URL до обработки format:

string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
                ? init.list?.firstpage : init.list?.uri)}";        // строка 50
else if (!string.IsNullOrEmpty(cat)) {
    var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat));
    string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-");
    if (!string.IsNullOrEmpty(eval)) { url = CSharpEval(...); }     // строки 78–85
    if (!url.StartsWith("http")) url = $"{init.host}/{url}";        // строка 87
}

Категория, стоящая ПЕРВОЙ в menu.categories, берёт базовый url из list.firstpage и format для неё не применяется. Симптом: первая категория отдаёт данные чужого файла (например все 360 фильмов из общего watchlist.json), у элементов img: null, id = ссылка на поиск Кинопоиска → в UI «здесь пусто».

Правило: list.uri/firstpage и первая категория в menu.categories должны указывать на один файл. Переставляешь категории — правь list.uri.

🔴 Питфолл №2: host + uri склеиваются

Строка 50 склеивает init.host + / + uri. Значит uri/firstpage обязаны быть относительными (watchlist-films.json), иначе двойной префикс: http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json → пусто.

🔴 Питфолл №3: main в YAML запрещён

CatalogSettings не имеет поля main для входного YAML → YamlDotNet давится неизвестным полем → ModInit.goInit() → null → ApiController.cs:70 отбрасывает сайт:

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

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

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

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

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

if (!string.IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href))   // ListController.cs

Элемент включается только если есть и name, и href. Симптом: из 404 позиций отдалось 52 — ровно те, у кого есть ссылка. Фикс: генератор подставляет fallback https://www.kinopoisk.ru/index.php?kp_query=<название>.

Как href превращается в id (карточка фильма)

ListController.cs:314–322:

else if (href.StartsWith("http"))
    href = Regex.Replace(href, "https?://[^/]+/", "");   // вырезает домен
...
id = href,

Поэтому полезно отдавать TMDB-URL: https://www.themoviedb.org/movie/687163 → id = "movie/687163" → CardController идёт в TMDB и отдаёт полноценную карточку (постер, жанры, описание, рекомендации). Работает для movie/N и tv/N.

CardController использует встроенный TMDB-прокси Lampac: http://{localhost}:{port}/tmdb/api/3/{cat}/{id}?api_key={CoreInit.conf.cub.api_key} — ключ уже внутри образа, отдельный TMDB-ключ для карточек не нужен.

🔴🔴 СЕДЬМАЯ СЕССИЯ: как РЕАЛЬНО внедрить секции в главную CUB (partNext)

Каталог и карточки закрыты. Рабочий механизм секций найден: перехват Lampa.Api.partNext.

Почему обёртка Api.sources.cub.main НЕ РАБОТАЕТ (three-strike)

Родной main$1() (@1257163 в app.min.js) объявлен внутри замыкания Lampa и опирается на локальные permit, get$7, Lang, permit.child_small, Helper$b:

function main$1() {
  var parts_limit = 6;
  var parts_data = [
    function (call) { get$7('?sort=now_playing', params, function (json) {
        json.title = Lang.translate('title_now_watch'); call(json); }, call, {life: day*2}); },
    function (call) { if (permit.child_small) return call(); ... }
  ];
  ...
}
Симптом Замер CDP Корень
cub.main({}) → вечный pending, 0 fetch {"pending":true} через 15 с permit = undefined вне замыкания → падение на первой проверке → callback не зовётся
секции не добавлялись, ошибок нет {"cubMainSrc":"...Manifest.catalogs['watchlist']..."} Manifest / Utils / network = undefined вне замыкания → ReferenceError → catch → родной фид
«Нет подключения к сети» в логе ТВ {localhost}/catalog?uid=...&token={token} Utils.account() без Manifest.api_host подставляет {localhost}

🔴 Вывод: cub.main невызываем извне by design. Обёртка — тупик, не «недоработка».

✅ Правильный механизм: Lampa.Api.partNext

// глобальная функция Lampa; CUB передаёт сюда parts_data — массив функций-секций
function partNext(parts, parts_limit, partLoaded, partEmpty) {
  var pieces = parts.filter(function (p) { return typeof p == 'function'; })
                     .slice(0, 0 + parts_limit);
  if (pieces.length) {
    new Progress().append(pieces).start(function (result) {
      var data = result.filter(function (r) { return r && r.results && r.results.length; });
      ...
    });
  } else partEmpty();
}

Патч добавляет свои функции-секции в parts до вызова оригинала:

Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
    if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
        var menu = window.__wl_menu;
        if (menu && Object.keys(menu).length) {
            parts.__wl_done = true;
            Object.keys(menu).forEach(function (name) {
                parts.push(wlSection(name, menu[name]));
            });
        }
    }
    return orig.apply(this, arguments);
};

Секция = функция call(json), теми же контрактами, что родные CUB:

function wlSection(name, url) {
    return function (call) {
        wlGet(url, function (json) {
            json.title = name;
            json.source = 'watchlist';                    // ← обязательно, иначе карточка не найдётся
            if (Lampa.Arrays.isArray(json.results))
                json.results.forEach(function (it) { it.source = 'watchlist'; });
            call(json);
        }, function () { call(); });                      // ошибка → пустой вызов (не ломает пул)
    };
}

Преимущество: работает на любой странице, независимо от источника, без permit, без params. Родной CUB не подменяется — секции добавляются в общий пул.

🔴 Только ГЛОБАЛЬНЫЕ объекты — внутри замыкания Manifest/Utils/network нет

Нужно ❌ Не работает в этом контексте ✅ Работает
api_host Manifest.api_host Lampa.Manifest.api_host (фолбэк — хардкод http://192.168.1.14:9118)
URL-склейка Utils.account(url) → {localhost} ручная: wlAbs(url) = host + path
HTTP network.silent(...) new Lampa.Reguest().silent(absURL, ...), фолбэк fetch
список секций Manifest.catalogs['watchlist'].menu window.__wl_menu (заполняется в loadCatalogs из /catalog)
function wlAbs(url) {
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    return wlHost() + (url.charAt(0) === '/' ? url : '/' + url);
}

Сборщик build_plugin.py

cd ~/Developer/lampac-scripts/scripts
python3 build_plugin.py     # plugin-patched.js + INJECT → plugin-cub.js
node --check plugin-cub.js

🔴 2026-09-25: сборщик переехал из /Users/admin/lampac-tmp/ в репозиторий lampac-scripts/scripts/ — вместе с собранным plugin-cub.js. Скрэтч-папка lampac-tmp вне git, файлы там не переживут чистку. Коммит: 5e55a47.

Детерминирован: режет [до startPlugin] + INJECT + [новый startPlugin] + '})();', проверяет инварианты. Текущий набор (12): INJECT=1, partNext def=1, wlInject def=1, unshift (не push)=1, push в INJECT=0, startPlugin=1, нет network.silent в INJECT=0, плюс добавленные 2026-09-25 — wlOnMain def=1, wlOnMain guard=1, wlUrl def=1, wlUrl верхний уровень=1.

🔴 Правило: новая функция в INJECT = новый инвариант в checks{}. Иначе регресс (например, потерянный фильтр по экрану) пройдёт сборку молча и всплывёт только у Alex в UI.

🔴 Питфолл прошлого сборщика patch_cub_inject.py: проверки "X" not in s шли по уже модифицированной строке → повторный запуск вставлял INJECT дважды → плюс лишняя } от вырезания → SyntaxError: Unexpected token '}' при балансе скобок {: 123, }: 122. Всегда проверять node --check + баланс скобок ПЕРЕД заливкой.

🔴 CDP-пробы на ТВ (порт 9998) — основной инструмент отладки

WS=$(curl -s "http://192.168.1.70:9998/json/list" \
     | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl')
python3 probe_ui.py "$WS"

Питон-клиент: websocket-client (import websocket). Модуль websockets не установлен. Runtime.evaluate с returnByValue: True, awaitPromise: True.

Скрипт Назначение
probe_ui.py приёмка: Lampa.Router.call('main') → чтение заголовков из DOM
probe_inject.py инжект plugin-cub.js в рантайм без деплоя (быстрая итерация)
probe_globals.py видимость Manifest/Utils/network
probe_cub_main.py сколько секций отдаёт cub.main
probe_wait.py ждёт результат + перехват console.log
probe_tv.py, probe_cache.py, probe_calls.py состояние, кэш, перехват fetch

🔴 eval() внутри IIFE не создаёт глобальных функций. После eval(pluginCode) wlHost() снаружи → ReferenceError: wlHost is not defined. Это нормально — cub.main их видит (одно замыкание), внешние пробы нет. Проверять только через Lampa.Api.main() либо чтение DOM.

🔴 Перезапуск Lampa на ТВ: luna-send требует файла, не inline-JSON

# ❌ печатает help: аргументы JSON не проходят через ssh + script -q -c
ssh root@192.168.1.70 'script -q -c "luna-send ... -a {\"id\":\"com.lampac.tv\"}" /dev/null'

# ✅ положить файл на ТВ и выполнить
scp tv_restart.sh root@192.168.1.70:/tmp/
ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
#!/bin/sh
luna-send -n 1 -f luna://com.webos.applicationManager/close '{"id":"com.lampac.tv"}'
sleep 5
luna-send -n 1 -f luna://com.webos.applicationManager/launch '{"id":"com.lampac.tv"}'

🔴 docker restart lampac НЕ перезагружает плагин в памяти ТВ (catalog.js кэширован браузером webOS). Признак: String(Lampa.Api.sources.cub.main) содержит старый код. Полный порядок деплоя: scp plugin.js → chmod 644/chown 1000:1000 → rm ._* → docker restart lampac (сброс Staticache) → sh /tmp/tv_restart.sh → probe_ui.py.

🔴 Питфолл №6: карточка без card_parse даёт HTTP 500

Без блока card_parse Catalog строит URL карточки как $"{init.host}/{uri}" (CardController.cs:33) → http://127.0.0.1:9118/movie/424781 — такого пути нет, html == null → BadRequest("html") → HTTP 500, в UI «здесь пусто».

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

[Route("catalog/card")]
public async Task<ActionResult> Index(string plugin, string uri, string type)

🔴 Параметра cat НЕТ. Карточка знает только uri и type. Поэтому initUrl: $"…/cards-{cat}.json" даёт cards-.json → 404 → HTTP 500.

Что можно и что нельзя подставлять:

Поле Механика Подстановка
initUrl CSharpEval.Execute<string> с CatalogInitUrlCard(host, args, uri, query, type) ✅ uri, type, host
node json.SelectToken(parse.node) — чистый JSONPath ❌ статичен
name/image/description ModInit.nodeValue(...) → attribute —

Питфолл JSONPath: SelectToken("movie/424781") вернёт null — / в JSONPath это вложенность (movie → 424781). Симптом: HTTP 400, тело parse.node. Слэш в ключе JSON недопустим для SelectToken.

Рабочая схема (когда карточку отдаёт наш JSON, а не TMDB):

card_parse:
  initUrl: |
    return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
  node: card          # фиксированный путь в файле
  name:          { attribute: name }
  original_name: { attribute: original }
  year:          { attribute: year }
  image:         { attribute: poster }
  description:   { attribute: overview }

Файлы wwwroot/cards/card-<media>-<id>.json (по одному на фильм) с обёрткой {"card": {...}}. По одному файлу — потому что node статичен и один общий файл по uri не отфильтровать.

Диагностика (ошибки карточки в логи Lampac НЕ пишутся!):

curl -s -o /tmp/c.json -w "HTTP %{http_code}\n" \
  "http://192.168.1.14:9118/catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie"
# тела-маркеры: "html" (URL не найден) | "parse.node" (node не нашёл узел) | "json" (не JSON)

🔴 Питфолл №5: кэш Catalog

Ключ: catalog:{plugin}:{search}:{sort}:{cat}:{page} — не зависит от содержимого файлов. hybridCache живёт в памяти процесса (не в cache/fdb/, не сбрасывается рестартом контейнера — проверено). После перегенерации JSON выдача может не меняться, пока ключ тот же. Приём ?nocache={Guid.NewGuid()} в format не помогает.

Практический вывод: при отладке менять сам URL/ключ (другой cat или sort), а не надеяться на инвалидацию кэша.

plugin.js — клиентская часть

Лежит в образе (/lampac/module/Catalog/plugin.js), не на HDD. Что делает:

  • main() — итерирует catalog.main, собирает секции СВЕРХУ родного каталога. 🔴 НО: работает это только при выбранном нашем источнике, потому что Api.getCatalog() читает манифест текущего источника. Для секций поверх CUB нужен перехват Lampa.Api.partNext — см. «СЕДЬМАЯ СЕССИЯ» выше.
  • menu() — итерирует catalog.menu из YAML
  • full()/list()/search() — карточка, категория, поиск
  • Lampa.Listener.follow('menu', ...) + e.abort() — перехват кликов по родным разделам (movie/tv/cartoon/anime/relise). Срабатывает только когда выбран источник «Мой список», поэтому родной CUB не трогается при других источниках
  • loadCatalogs() — сохраняет window.__wl_menu из /catalog (нужно патчу partNext)

🔴 Питфолл №6: источник не виден в UI (enumerable: false)

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

Свойство создаётся не-enumerable → Object.keys(Lampa.Api.sources) его не видит → Lampa не рисует источник в панели выбора. При этом Api.sources['Мой список'] доступен напрямую и каталог отдаёт. Замер через DevTools ТВ (http://192.168.1.70:9998/json) → {"enumerable":false,"configurable":false}.

Фикс: добавить enumerable:true,configurable:true в оба вхождения. Правка применяется скриптом patch_plugin.py (не sed/grep), проверка синтаксиса — node --check перед заливкой. Монтируется через compose (см. выше).

Сканирование sites/

Path.Combine(AppContext.BaseDirectory, ModInit.modpath, "sites") — папка сканируется на старте контейнера. Кэш-фикс: docker restart lampac (старый подход) не всегда достаточен — см. питфолл №5.

Управление

ssh kraken
cd /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac

docker compose up -d
docker compose logs -f
docker compose restart
docker compose down

Правка docker-compose.yml — без sudo (каталог kraken:kraken). Создание новых каталогов в корне диска — через sudo (корень root:root). У kraken sudo без пароля.

Развёртывание каталога watchlist (deploy_lampac.py)

🔴 2026-09-25: файл в репозитории, не в скрэтче — ~/Developer/lampac-scripts/scripts/deploy_lampac.py. 7 шагов с бэкапами и самопроверкой. Запуск:

python3 ~/Developer/lampac-scripts/scripts/deploy_lampac.py          # полный цикл
python3 ~/Developer/lampac-scripts/scripts/deploy_lampac.py --skip-plugin   # только JSON+YAML

⚠️ Для скриптов прошлых сессий (пробники, разовые патчи) lampac-tmp остаётся упомянут в исторических разделах ниже — это скрэтч вне git, ссылки оставлены как есть. Источник истины для плагина каталога и генератора — только репозиторий.

Порядок: бэкап JSON в wwwroot/.bak/ → заливка 5 JSON → watchlist.yaml → патченный plugin.js → volume в compose (если нет) → docker compose up -d → проверка (/catalog, 4 категории с подсчётом постеров и TMDB-ID, факт патча).

Генератор каталога (gen_watchlist.py)

⚠️ Устарело 2026-09-24: генератор выпилен из пайплайна — фид Lampa строит watchlist-sync sync-down (см. personal/tech/watchlist-sync-feed). Файл остаётся в репе как исторический: ~/Developer/lampac-scripts/scripts/gen_watchlist.py (бывш. gen_watchlist_v2.py, скрэтч-копии в lampac-tmp — не источник истины).

Читает .md, резолвит каждый фильм: сначала Radarr (мгновенно, там уже tmdbId и постер), затем TMDB по названию (2 языка × movie/tv), фолбэк — ссылка Кинопоиска. Кэш ~/.lampac_tmdb_cache.json, ключ title|year.

Первый прогон: 330 постеров из 403 (Radarr 7, TMDB 327, фолбэк 69). Занимает 8–10 минут (403 фильма × TMDB, sleep(0.02)). 🔴 Запускать в background — foreground упрётся в лимит 600 с.

Требует TMDB_KEY из /Users/admin/Developer/watchlist-sync/.env. ⚠️ TMDB-хук api.themoviedb.org с Кра́кена доступен (проверено: 401 без ключа = сеть есть).

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

curl -sS --max-time 20 "http://localhost:9118/version?type=hash"
# → 46b29e1fe8fa6021cd83e75c41abffb7   (для текущего :latest)

По сети: http://192.168.1.14:9118/version?type=hash — то же значение.

⚠️ Питфоллы

1. Первый старт долгий (~4 минуты)

Контейнер компилирует и грузит ~40 native-модулей: compilation LampaWeb, DLNA, GStreamer, Sync, TorrServer, ... затем loaded module: <имя>.

Пока идёт загрузка, HTTP рвёт соединение:

curl: (56) Recv failure: Connection reset by peer

🔴 health: healthy не означает готовность HTTP. Статус встаёт раньше. Признак готовности — лог Configure complete (идёт после последнего loaded module:).

Не перезапускать контейнер, не считать это поломкой. Просто ждать.

2. docker manifest inspect с Mac отдаёт denied

GHCR требует авторизацию при обращении с Mac. Проверять наличие linux/arm64 из Кра́кена:

ssh kraken 'docker manifest inspect ghcr.io/lampac-nextgen/lampac:latest' | jq -r '.manifests[].platform | .os + "/" + .architecture'
# linux/amd64
# linux/arm64      ← есть, сборка из исходников не нужна
# unknown/unknown
# unknown/unknown

3. Шаблон логов первого старта

compilation Alloha
compilation LampaWeb
...
loaded module: Vibix
loaded module: TorrServer
...
Configure complete          ← HTTP начинает отвечать

4. 🔴 sudo через ssh без tty падает

ssh kraken "sudo docker exec lampac ls /lampac"
# sudo: a terminal is required to read the password;
#       either use the -S option ... or configure an askpass helper

Фикс: флаг -t. Но ⚠️ при -t нельзя передавать a && b — цепочка команд рвёт tty-сессию (Connection to kraken closed), и первая же команда отваливается. Каждую команду вызывать отдельно:

def sh(cmd):
    return subprocess.run(f"ssh -t kraken {cmd}", shell=True, ...)

sh("sudo cp /tmp/x.json /srv/.../wwwroot/x.json")   # ✅ одна команда
sh("sudo chown kraken:kraken /srv/.../wwwroot/x.json")
sh("sudo chmod 644 /srv/.../wwwroot/x.json")
# ❌ sh("sudo cp … && sudo chown … && sudo chmod …")  — рвёт tty

5. 🔴 docker compose up -d не пересоздаёт контейнер при смене volumes

Изменение только списка volumes в docker-compose.yml не приводит к пересозданию: compose up -d считает конфигурацию контейнера неизменной. Симптом: новый bind-mount не появился, патч plugin.js не виден внутри.

Проверка:

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

Фикс:

ssh kraken "cd /srv/dev-disk-by-uuid-6194539b-.../lampac && sudo docker compose up -d --force-recreate"

Сеть

Хост IP
Кра́кен (eth0) 192.168.1.14 (второй адрес 192.168.1.15)
ТВ LG 192.168.1.70
Mac Alex 192.168.1.57

Один сегмент 192.168.1.x — туннели и проброс портов не нужны. Подробнее: family/how-to/kraken-network.

Модуль Catalog — каталоги из YAML

Модуль Catalog (в образе, по умолчанию выключен) отдаёт свои каталоги как плагин Lampa. Разобрано по коду и включено в работу 2026-09-22 — см. personal/plans/lampac-watchlist-plugin.

Наш каталог «Мой список»

Отдаёт movies-watchlist.md как каталог в Lampa. Файлы:

Путь на HDD (.../lampac/) Роль
config-module/init.conf включает Catalog (монтируется в /lampac/init.conf:ro)
catalog/sites/watchlist.yaml описание каталога для Catalog
catalog/override/ переопределения (пусто)
wwwroot/watchlist.json + watchlist-<code>.json JSON-данные (Lampac отдаёт по HTTP)
scripts/gen_watchlist.py генератор: .md → JSON
~/scripts/watchlist-catalog.sh обёртка + перезапуск (сброс кэша), cron 02:30

Плагин на ТВ: http://192.168.1.14:9118/catalog.js → источник «Мой список».

⏳ Переделывается (2026-09-22). Текущая реализация признана неверной: заполнен блок menu (подменяет родной каталог CUB вместо добавления секций сверху), а данные берутся парсингом .md вместо Radarr API (в .md нет TMDB ID и постеров). Разбор ошибки — personal/plans/lampac-watchlist-plugin, секция «🔴 АРХИТЕКТУРНАЯ ОШИБКА».

🔴 Рабочая форма sites/watchlist.yaml (проверена 2026-09-22, откат после провала)

enable: true
jsonPath: true
displayindex: 1
displayname: Мой список
host: http://127.0.0.1:9118

menu:                       # ← ОБЯЗАТЕЛЕН (регистрирует канал), единственный блок категорий
  - categories:
      Фильмы: films
      Новинки: new
      Мультфильмы: cartoons
      Аниме: anime
    format:
      "-": |
        return $"http://127.0.0.1:9118/watchlist-{cat}.json";

search:
  uri: watchlist.json

list:
  uri: watchlist-films.json
  firstpage: watchlist-films.json
  total_pages: 1

content:
  nodes: "$.items[*]"
  name:          { attribute: name }
  original_name: { attribute: original }
  year:          { attribute: year }
  href:          { attribute: href }
  image:         { attribute: image }
  args:
    - { name_arg: rating, attribute: rating }

🔴 main в YAML ЗАПРЕЩЁН — ломает канал (опровергнуто 2026-09-22)

Раздел ниже (гипотеза «menu подменяет / main добавляет») — НЕВЕРЕН. Проверено на живом сервере: попытка добавить блок main в YAML → «Мой список» исчез из /catalog, catalog/list?plugin=watchlist отдавал пустоту. Три раунда правок, три рестарта.

Гипотеза (неверна) Факт (проверено)
menu подменяет родные разделы ❌ menu регистрирует канал. ApiController.cs:70: if (init == null || !init.enable || init.menu == null || init.hide) continue; — без menu сайт отбрасывается
main добавляет секции сверху ❌ блока main не существует во входном YAML. YamlDotNet давится неизвестным полем → ModInit.goInit(site) → null → сайт молча пропадает
Заполнять нужно только main ❌ main Catalog генерирует САМ из menu. В JSON-ответе /catalog есть оба ключа с одинаковым содержимым

⚠️ Обе формы main ломают одинаково — и список, и словарь:

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

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

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

Lampa.Listener.follow('menu', function(e){
    if (e.type=='action' && (e.action=='tv'||'cartoon'||'anime'||'movie'||'relise')) {
        var catalog = Api.getCatalog();
        if (!catalog) return;
        ...
        Lampa.Router.call('category', {...});
        e.abort();          // ← перехват клика по родному разделу
    }
});

Перехват срабатывает ТОЛЬКО когда выбран источник «Мой список». Родной CUB/tmdb не трогается, пока источник не переключён на наш.

🔴 host + uri СКЛЕИВАЮТСЯ — uri обязан быть относительным

ListController.cs:53:

string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
                ? init.list?.firstpage : init.list?.uri)}";

Полный URL в uri/firstpage даёт двойной префикс → пусто:

http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json

Правильно: только watchlist-films.json, host приклеится сам. Так же устроены родные YAML (vokino.yaml → uri: search?name={search}).

⚠️ Питфолл: явный format в menu перезагружает страницу

С format-блоком в menu Lampa перезагружает прогрузку страницы при каждом выборе раздела (наблюдение Alex). Если перезагрузка не нужна — убрать format из menu, оставить только categories.

🎯 Как добиться «CUB + мой список сверху» — ✅ РЕШЕНО 2026-09-22

Родной CUB и «Мой список» — это два источника Lampa, переключаются в «остальное → основной источник». Одновременно показать нельзя без вмешательства:

Путь Реализуемость
Блок main в YAML ❌ невозможно — ломает парсинг
Оставить menu как есть ⚠️ источник работает, но разделы подменяются, пока он выбран
Патч plugin.js — обёртка cub.main ❌ тупик: cub.main невызываем извне (permit в замыкании)
Патч plugin.js — перехват Lampa.Api.partNext ✅ РАБОТАЕТ — проверено на живом ТВ через CDP

✅ Итоговое решение — wlPatchPartNext(). partNext глобальный, принимает пул функций-секций; наши добавляются в тот же пул, что и родные CUB. Работает на любой странице, независимо от источника. Реализация, питфоллы и probe-скрипты — раздел «🔴🔴 СЕДЬМАЯ СЕССИЯ: как РЕАЛЬНО внедрить секции в главную CUB» выше.

⚠️ Баг Lampac: источник не виден в списке выбора

plugin.js, строка 59 (loadCatalogs()), два вхождения:

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

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

Фикс: { get: ..., enumerable: true, configurable: true }.

plugin.js живёт в образе, не на HDD (docker inspect — монтируются только catalog/sites и catalog/override). Правка через docker exec теряется при пересоздании. Постоянный путь — volume:

volumes:
  - ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro

Проверка синтаксиса ПЕРЕД заливкой: node --check /tmp/catalog-plugin.js. Статус: не применено, ждёт апрува Alex.

Radarr как источник данных (постеры, TMDB ID) — ✅ ПРОВЕРЕНО 2026-09-22

Фильмы уже резолвлены в Radarr — там есть TMDB ID и постеры. Это правильный источник для каталога; .md их не содержит.

# API-ключ Radarr
grep -oE "<ApiKey>[^<]+</ApiKey>" \
  /srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config/config.xml
# → cbcb8ec320104359920c0f611d503c7b

# ⚠️ -L обязателен, иначе 307 и пустое тело
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length   # → 50

# Sonarr, порт 8989, ключ 15fbec32f1cb4b05b1c2b229ce27359b
curl -sL "http://localhost:8989/api/v3/series?apikey=$SKEY" | jq length  # → 13

Поля ответа Radarr (для каталога нужны все): tmdbId, imdbId, title, originalTitle, year, titleSlug, hasFile, genres, ratings.tmdb.value, images[] | select(.coverType=="poster") | .remoteUrl. Кэш постеров на диске: Radarr /config/MediaCover/ (46 каталогов).

{
  "tmdbId": 1292695, "imdbId": "tt31728330",
  "title": "They Will Kill You", "year": 2026,
  "hasFile": true, "genres": ["Horror"],
  "ratings": {"tmdb": {"value": 6.945}},
  "images": [{"coverType": "poster",
              "remoteUrl": "https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28Ruu6.jpg"}]
}

⚠️ Названия в Radarr латиницей (They Will Kill You), в .md — по-русски. Перевод делает TMDB Proxy (proxy_tmdb = true в lampainit.js), уже включён.

⚠️ Связки kp ↔ tmdb в данных нет. В .md — 68 ссылок kinopoisk.ru/film/<id>/, в Radarr — tmdbId. Прямого соответствия нет ни в одном файле. Мост — модуль watchlist_sync/resolver.py (см. ниже).

watchlist_sync/resolver.py — резолвер постеров (уже написан!)

Путь: /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync/watchlist_sync/

Уже решает задачу постеров/ID для каталога:

  • поиск в TMDB (api.themoviedb.org/3, языки ru-RU + en-US)
  • поиск в Кинопоиске (api.poiskkino.dev/v1.4)
  • возвращает Candidate: tmdb_id, kp_id, title, year, score, rating, genres
  • is_horror() — ужасы по TMDB genre_id=27 (нужно для сортировки 🧟‍♀️)
  • кэш .resolve_cache.json; различает ok / ambiguous (score < 0.75) / not_found

🔴 watchlist_sync/arr.py — ИСТОЧНИК АВТОСКАЧКИ (в коде!)

# radarr_add():
"monitored":       True,
"addOptions":      {"searchForMovie": True},          # ← качает СРАЗУ после добавления

# sonarr_add():
"monitored":       True,
"addOptions":      {"searchForMissingEpisodes": True}, # ← то же для сериалов

⚠️ Даже если снять monitored вручную, следующий запуск добавления из списка включит автоскачку снова — флаг зашит в код. Фикс: searchForMovie → False, searchForMissingEpisodes → False. Статус: не применён, на апруве.

Как выключить автоскачку на HDD — ✅ ВЫПОЛНЕНО 2026-09-22

Сервис Очередь monitored rssSyncInterval
Radarr 6 → 0 45 → 0 30 → 0
Sonarr 19 → 0 13 → 0 15 → 0

Метаданные целы: 50 фильмов, 46 с постерами, 50 с tmdbId и рейтингом, 24 файла на диске не тронуты. Бэкапы — метка 20260922-123404.

Проверка пассивности: Transmission за 45 с — NET 290GB/26.7GB, BLOCK 214GB/393kB не сдвинулись. Логи Radarr (search|download|grab) за 2 мин — пусто.

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

# отмена загрузки — файлы на диске НЕ трогаются
DELETE /api/v3/queue/<id>?removeFromClient=true&blocklist=false

# снятие monitored — КЛЮЧ ПОЛЯ РАЗНЫЙ!
PUT /api/v3/movie/editor   {"movieIds": [...], "monitored": false}    # Radarr
PUT /api/v3/series/editor  {"seriesIds": [...], "monitored": false}   # Sonarr, НЕ "ids"

# RSS off — отправлять ВЕСЬ объект конфига с id, иначе молча вернёт старое
PUT /api/v3/config/indexer {"rssSyncInterval": 0, "id": 1, ...остальное}

Откат:

curl -sL -X PUT ".../api/v3/movie/editor"  -d '{"movieIds":[...],"monitored":true}'
curl -sL -X PUT ".../api/v3/series/editor" -d '{"seriesIds":[...],"monitored":true}'
curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1,...}'
cp radarr.db.bak-20260922-123404 radarr.db && docker restart radarr

🔴 Питфолл: urllib не идёт по 307, curl -sL идёт

Симптом: все DELETE/PUT из Python-скриптов вернули HTTP Error 307: Temporary Redirect, ничего не изменилось. Штатный urllib на 307/308 превращает DELETE в GET и теряет тело.

Фикс — свой redirect-хендлер:

class RedirectKeepMethod(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        if code in (307, 308):
            new = urllib.request.Request(newurl, data=req.data,
                                         method=req.get_method())
            for k, v in req.headers.items():
                new.add_header(k, v)
            return new
        return super().redirect_request(req, fp, code, msg, headers, newurl)

_OPENER = urllib.request.build_opener(RedirectKeepMethod)
# далее _OPENER.open(req) вместо urllib.request.urlopen(req)

Прочие грабли сессии:

  • Sonarr группирует эпизоды сезона под одним id — после удаления первой записи остальные дают 404, это не сбой, они ушли с родительской.
  • Кириллица внутри inline-jq в ssh '...' ломает команду (unexpected INVALID_CHARACTER). Ключи объекта — латиницей: total, monitored, queue.

TorrServer внутри Lampac — на HDD НЕ пишет (проверено 2026-09-22)

Параметр Значение Смысл
UseDisk false 🔴 на диск не пишет вообще
CacheSize 100663296 (96 МБ) кэш в RAM
TorrentsSavePath "" пути сохранения нет
TorrentDisconnectTimeout 180 раздача живёт 3 мин после остановки
# конфиг TorrServer
docker exec lampac cat /lampac/data/ts/settings.json | jq '.BitTorr | {UseDisk, CacheSize, TorrentsSavePath}'

# жив ли
curl -s http://192.168.1.14:9118/ts/echo          # → MatriX.135
curl -s -X POST http://192.168.1.14:9118/ts/torrents \
  -H "Content-Type: application/json" -d '{"action":"list"}'   # → []

🔴 Поиск торрентов: отдельного Jackett НЕТ, его роль играет Lampac

lampainit.js прописывает:

Lampa.Storage.set('parser_torrent_type', 'jackett');
Lampa.Storage.set('jackett_url', '192.168.1.14:9118');   // ← сам Lampac!
Lampa.Storage.set('jackett_key', '1');

Контейнера Jackett в системе нет. Lampac отдаёт Jackett-совместимый API сам:

# ⚠️ на КОРНЕ, БЕЗ префикса /jackett (с ним → 404)
curl "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix"
# → {"Results":[{"Tracker":"rutor","Title":"...","MagnetUri":"magnet:?xt=...","Seeders":0}]}

✅ Проверено: вернул реальные результаты с магнит-ссылками. Отдельный Jackett ставить НЕ нужно — так задумано производителем.

Занятые порты на Кра́кене (2026-09-22)

7878 Radarr · 8096 Jellyfin · 8191 FlareSolverr · 8989 Sonarr · 9000 Portainer · 9091 Transmission · 9696 Prowlarr · 51413 Transmission (UDP) · 9118 Lampac

Отладка UI Lampa на ТВ (webOS)

Правки по SSH не сделать, но состояние читается через Chrome DevTools Protocol:

# webOS отдаёт DevTools на порту 9998 (WebAppMgr --remote-debugging-port=9998)
curl -s http://192.168.1.70:9998/json | jq -r '.[] | "\(.type): \(.url)"'
# → file:///.../com.lampac.tv/index.html?card=...&source=cub

Дальше websocket + Runtime.evaluate читает живые переменные:

Выражение Что даёт
Lampa.Storage.field('source') выбранный источник
Object.keys(Lampa.Api.sources).join('|') перечисляемые источники
Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список') enumerable/configurable
Lampa.Api.sources['Мой список'].getCatalog() ключи каталога

Скрипты-пробы: /tmp/lampa_probe.py, /tmp/lampa_probe2.py, /tmp/lampa_probe3.py.

⚠️ tcpdump на Кра́кене нет. luna://com.webos.service.capture/takeScreenShot на этом ТВ не существует. luna-send пишет в tty → нужен script -q -c. Подробности: family/how-to/tv-lampac-plugin.

Общее про модуль

Что Как
Данные sites/<site>.yaml — host, menu, search, list, content
Переопределения override/<site>.yaml, override/_.yaml
Роуты /catalog.js, /catalog, /catalog/list, /catalog/card
WAF ^/catalog/ → 10 req/sec
Примеры 10 файлов: vokino, rezka, kinopub, aniliberty, eneyida, filmix, filmixapp, getstv, kinobase, kinoukr

Обязателен HTTP: if (!url.StartsWith("http")) url = $"{init.host}/{url}" — локальные файлы не читаются.

Два режима: jsonPath: true → JToken.Parse (JSON), иначе HtmlAgilityPack (HTML).

🔴 Питфолл: элемент с пустым href ВЫБРАСЫВАЕТСЯ — if (!IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href)) (ListController.cs). Нужен непустой href у каждого элемента.

🔴 Питфолл: {cat} НЕ подставляется в nodes — JSONPath статичен. Разные категории = разные JSON-файлы.

🔴 Питфолл: кэш без версии данных — ключ catalog:{plugin}:{search}:{sort}:{cat}:{page}. После правки данных нужен docker restart lampac.

Выключен в двух местах base.conf/current.conf:

"BaseModule": { "SkipModules": [ "Catalog", ... ] }
"LampaWeb": { "initPlugins": { "catalog": false } }

Синапс модулей (всего 33): AdminPanel, Adult, Catalog, Community, DLNA, DatabaseEditor, ExternalBind, ForkPlayerXML, GStreamer, JacRed, Kit, LampaWeb, LampacApk, LogUserRequest-Lite, MsxNative, Music, NextHUB, Online, OnlineAnime, OnlineENG, OnlineGEO, OnlinePaid, OnlineRUS, OnlineUKR, PidTor, Potok, Proxy, SISI, Sync, Telemetry, Tracks, Transcoding, WebLog.

Полный пример YAML (vokino.yaml): docker exec lampac cat /lampac/module/Catalog/sites/vokino.yaml.

Выбор порта — docker compose config + sudo ss -tlnp перед деплоем.

Интеграция с Lampa (ТВ) — ✅ СДЕЛАНО 2026-09-22

  • Плагин: http://192.168.1.14:9118/online.js
  • TorrServer в Lampa: Настройки → TorrServer → «Использовать основную ссылку» → http://192.168.1.14:9118
  • Буфер плеера: 50–100 МБ
  • /lg.ipk отдаёт сам Lampac (проверено: HTTP 200)

Установка клиента на LG webOS — по SSH, без флешки

🔴 Media Station X и .ipk с флешки — НЕ нужны. ТВ имеет SSH-доступ (root), установка делается с Mac.

ТВ: root@192.168.1.70, ключ ~/.ssh/konka_webos, webOS 5.4.2 (ядро 4.4.84, image starfish-dvb-secured, build 2602, webos_build_datetime 20230405092501). Порты открыты: 22 (SSH), 3000/3001 (Luna). Из кастомного уже стоит: org.webosbrew.hbchannel 0.7.2, org.webosbrew.inputhook, me.wouterdek.magic4pc.

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

# 1. Скачать .ipk от Lampac прямо на ТВ
ssh root@192.168.1.70 'curl -sS --max-time 60 -o /tmp/lampa.ipk "http://192.168.1.14:9118/lg.ipk"'
# → 6 896 874 байт, формат ar (!<arch>), НЕ tar/gzip

# 2. Установить (обязательна обёртка script — см. питфолл ниже)
ssh root@192.168.1.70 'rm -f /tmp/inst.txt; script -q -c \
  "luna-send -n 1 -f luna://com.webos.appInstallService/dev/install \
   {\"id\":\"org.lampa.tv\",\"ipkUrl\":\"/tmp/lampa.ipk\",\"subscribe\":true}" \
  /tmp/inst.txt >/dev/null 2>&1; cat /tmp/inst.txt'
# → {"subscribed":true,"returnValue":true}

# 3. Проверить, что установилось
ssh root@192.168.1.70 'ls /media/developer/apps/usr/palm/applications/'
# → com.lampac.tv   ← имя приложения Lampa, НЕ org.lampa.tv

# 4. Запустить
ssh root@192.168.1.70 'rm -f /tmp/l.txt; script -q -c \
  "luna-send -n 1 -f luna://com.webos.applicationManager/launch {\"id\":\"com.lampac.tv\"}" /tmp/l.txt'

# 5. Убедиться, что открылось
ssh root@192.168.1.70 'rm -f /tmp/f.txt; script -q -c \
  "luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}" /tmp/f.txt; grep appId /tmp/f.txt'
# → "appId": "com.lampac.tv"

Фактический ID приложения — com.lampac.tv (vendor Lampac, version 1.0.0, type web). ⚠️ В luna-send ... dev/install поле "id" — произвольная метка; реальный appId берётся из appinfo.json внутри ipk. Ошибиться здесь не страшно, но искать приложение надо по фактическому ID.

🔴 Питфолл: luna-send по SSH молчит

Симптом: любой вызов luna-send через ssh root@TV "luna-send ..." печатает хелп-экран, а не результат. С -t (PTY) — тоже. С -a '{"json"}' — тоже.

Причина: luna-send детектирует TTY и отказывается писать в pipe.

Решение: обернуть в script(1), который аллоцирует псевдо-TTY, и прочитать временный файл:

ssh root@TV 'script -q -c "luna-send -n 1 -f luna://service/method {json}" /tmp/o.txt; cat /tmp/o.txt'

Готовый враппер: ~/Automation/tv-luna-send.sh (TV_HOST переопределяет таргет). Доки: family/how-to/tv-luna-send, family/how-to/htpc-webos-luna.

⚠️ Установка идёт через com.webos.appInstallService/dev/install, а НЕ через org.webosbrew.hbchannel.service/install (последний в образе есть, но luna-send его не дёрнул чисто).

⚠️ luna-send -i (subscribe, бесконечный) через враппер не работает — нужен polling.

Первый просмотр — проверено 2026-09-22

Фильм запущен, не дёргается. Alex: «Вроде стабильно, не делается».

Факт Значение
Соединения ТВ → Lampac 3 × ESTABLISHED 192.168.1.70 → 192.168.1.14:9118
Нагрузка контейнера lampac CPU 57%, NET 168 МБ ↓ / 132 МБ ↑
Битрейт на eth0 ~3–4 Мбит/с

⚠️ Почему всего 3–4 Мбит, а не 15: выбран источник «Смотреть онлайн (lampac 1.18…)» — встроенный плагин online.js, HLS-поток. Lampac отдаёт ссылку, ТВ тянет видео напрямую из CDN (в /proc/net/tcp ТВ видны внешние адреса на :443/:8443). Тяжёлый remux с торрента не участвует — поэтому и не дёргается.

🔴 Следствие: этот просмотр не проверяет, держит ли сеть remux 60–80 Мбит. Тест функциональности, не полосы. Полосу проверять на торрент-источнике.

Как убрать выбор источника: Lampa → Настройки → Плагины → выключить online.js. Останутся только торренты.

🔴 Выбор источника каталога в Lampa (проверено 2026-09-22)

В Lampa НЕТ пункта «Настройки → Источник». Я его выдумал — Alex дважды указал на ошибку. Не повторять.

Источник (Rezka / Kinobase / «Мой список» / …) переключается из меню каталогов на главной:

  1. Открыть Lampa на главной
  2. Встать на любой ряд каталога → нажать влево на пульте
  3. Слева выедет панель со списком источников
  4. Выбрать нужный → OK; каталог перестроится, в заголовке появится имя источника в верхнем регистре («Фильмы — МОЙ СПИСОК»)

Плагин catalog.js не добавляет пункт меню — он подменяет существующие. Механика: при старте приложения Lampa.Utils.account(api_host + '/catalog', …) получает список каталогов, регистрирует каждый как Lampa.Params.values.source[key] и подменяет Lampa.Api.sources[key]. Затем хук Lampa.Listener.follow('menu', …) перехватывает клики по movie/tv/cartoon/anime/relise и подставляет URL из catalogs[key].menu.

⚠️ Следствие — каталоги регистрируются ТОЛЬКО при старте приложения. Добавил плагин и не перезапустил Lampa → источника не будет, /catalog не запрашивается, соединений с :9118 ноль. Нужен полный перезапуск (не сворачивание).

⚠️ Клиент на ТВ — это не «отдельная Lampa»

com.lampac.tv (webOS) грузит UI с сервера: loader.js читает lampac_url из localStorage (дефолт http://192.168.1.14:9118) и подключает app.min.js + lampainit.js. Значит логика каталога на ТВ = серверная, и править её надо на Lampac, а не на ТВ.

/lampainit.js содержит var plugins_add = [...] со строкой {"url":".../catalog.js","name":"Альтернативные источники каталога","status":1} — это серверный список плагинов, которые Lampac сам подсовывает в UI. Каталог watchlist работает через модуль Catalog, не через эту строку. Не искать причину поломки в plugins_add.

🔴 БАГ Lampac: источник не виден в панели (enumerable: false)

Найдено 2026-09-22 диагностикой через Chrome DevTools ТВ. Симптом Alex: «Ни на главной ни в Фильмах нет! Там только Сейчас смотрят, рекомендуем итд».

Факты с живого ТВ:

Замер Значение
Lampa.Storage.field('source') 'Мой список' — источник уже выбран
Object.keys(Lampa.Api.sources) `'tmdb
Lampa.Api.sources['Мой список'].getCatalog() search, movie, cartoons, anime, menu, main — ✅ работает
Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список') {"enumerable":false,"configurable":false} ← корень

Корень — в /lampac/module/Catalog/plugin.js (строка 59, loadCatalogs(), два вхождения):

// БЫЛО (баг)
Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; } });
// НАДО
Object.defineProperty(Lampa.Api.sources, key,
  { get: function(){ return Api; }, enumerable: true, configurable: true });

Не-enumerable свойство не попадает в Object.keys() → Lampa не рисует источник в панели выбора. Баг Lampac, не нашей настройки.

🔴 plugin.js живёт в ОБРАЗЕ, не на HDD. docker inspect показывает монтирования только на catalog/sites и catalog/override. Правка через docker exec исчезнет при обновлении образа — нужен bind-mount:

# docker-compose.yml
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
ssh kraken 'docker exec lampac cat /lampac/module/Catalog/plugin.js' > /tmp/catalog-plugin.js
cp /tmp/catalog-plugin.js /tmp/catalog-plugin.js.bak-$(date +%Y%m%d-%H%M%S)
# правка двух вхождений defineProperty (только через patch, не sed)
node --check /tmp/catalog-plugin.js          # проверить синтаксис ДО заливки
scp /tmp/catalog-plugin.js kraken:/srv/.../lampac/catalog/plugin.js
ssh kraken 'cd /srv/.../lampac && docker compose up -d'

Статус: фикс НЕ применён, ждёт апрува Alex (2026-09-22). Полный разбор — personal/plans/lampac-watchlist-plugin, раздел «ГЛАВНЫЙ ПИТФОЛЛ».

🛠 Диагностика ТВ через Chrome DevTools — порт 9998

Главный инструмент для отладки UI Lampa. webOS отдаёт Chrome DevTools Protocol: у процессов WebAppMgr есть --remote-debugging-port=9998. Даёт чтение состояния живой Lampa — ни логи, ни tcpdump этого не дают.

# цель отладки + URL приложения
curl -s --max-time 8 "http://192.168.1.70:9998/json" | /usr/bin/jq -r '.[] | "\(.type): \(.url)"'
# → page: file:///media/developer/apps/usr/palm/applications/com.lampac.tv/index.html?...

Чтение переменных — websocket-client + Runtime.evaluate:

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

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

Ловушки:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Роут Код Что
/ 200 LampaWeb (UI, <base href="/lampa-main/">)
/online.js 200 плагин VOD — 70+ онлайн-источников
/lg.ipk 200 установщик клиента для LG (6.9 МБ)
/sisi 200 JSON-список платформ 18+
/dlna 200 DLNA-модуль
/ts/echo 200 TorrServer (отдаёт версию, напр. MatriX.135)
/catalog.js 200 плагин каталогов (включён 2026-09-22) — см. раздел Catalog
/catalog 200 JSON каналов каталогов
/watchlist.json, /watchlist-<code>.json 200 наш JSON-каталог watchlist
/cub, /api, /lampac.js 404/500 —

⚠️ wwwroot отдаётся статикой: любой файл, положенный туда, доступен по HTTP от корня (http://<host>:9118/<файл>). Проверено записью пробного файла.

Radarr/Sonarr как источник метаданных для каталога (2026-09-22)

Зачем: каталог watchlist нуждается в TMDB ID и постерах для элементов. Список movies-watchlist.md — просто текст, там этого нет. Но фильмы добавлялись в Radarr, который уже держит метаданные (тянет из TMDB при добавлении).

Что отдаёт Radarr API

KEY=$(ssh kraken 'grep -oE "<ApiKey>[^<]+</ApiKey>" \
  /srv/...6194539b.../docker/radarr/config/config.xml' | sed 's/<[^>]*>//g')

curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq .
{
  "tmdbId": 1292695,
  "title": "They Will Kill You",
  "year": 2026,
  "hasFile": true,
  "monitored": true,
  "images": [{"coverType": "poster",
              "remoteUrl": "https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28R"}]
}

Маппинг в формат каталога: tools.tmdbId → id/href, images[].remoteUrl → img, title+year → title, hasFile → признак «уже скачан».

🔴 Питфолл: curl -sL, без -L Radarr отдаёт 307

curl -s  "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length  # ❌ 0 байт
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length  # ✅ 50

Окружение arr-стека на Кра́кене

Сервис Порт Конфиг на HDD
radarr 7878 /srv/...6194539b.../docker/radarr/config/
sonarr 8989 аналогично
prowlarr 9696 аналогично
transmission — отдельный контейнер

Постеры складываются в /config/MediaCover/ (46 каталогов на 2026-09-22) — можно раздавать их статикой через wwwroot, не дёргая CDN TMDB.

⚠️ sqlite3 в контейнерах arr нет — БД (radarr.db) читать либо через strings, либо вытащить файл на хост (там /usr/bin/sqlite3 есть).

Управление автоскачкой (два рычага, оба нужны)

Рычаг Команда Эффект
monitored PUT /api/v3/movie/editor фильм не ищется/не качается, метаданные остаются
rssSyncInterval PUT /api/v3/config/indexer 0 = Radarr не ходит в RSS (сейчас 30 мин)

⚠️ В Radarr нет состояния «источник, но не грузить» — это ровно monitored: false.

Контур воспроизведения — не путать (✅ проверено 2026-09-22)

Слой Что делает Пишет на HDD
Radarr + Sonarr + Transmission автоскачка релизов в библиотеку да
Lampac TorrServer (встроенный) стриминг из UI Lampa по требованию ❌ нет — доказано
Catalog «Мой список» показ секций с постерами ❌ ничего не качает

Просмотр через Lampa не пополняет библиотеку: TorrServer отдаёт поток и выбрасывает. transmission к Lampa отношения не имеет.

Доказательство, что TorrServer не пишет на диск — /lampac/data/ts/settings.json:

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

UseDisk: false — дефолт Lampac, настраивать не надо. Кэш Lampac целиком: /lampac/cache/ = 31 МБ (SQLite, Playwright, gstreamer-registry — не фильмы), /lampac/data/ = 44 МБ. Список торрентов пуст: curl -s -X POST .../ts/torrents -d '{"action":"list"}' → [].

Живость TorrServer: curl -s "http://192.168.1.14:9118/ts/echo" → MatriX.135.

🔴 Поиск торрентов — Jackett внутри Lampac (отдельный НЕ нужен)

Ловушка: в lampainit.js прописано parser_torrent_type = 'jackett', но контейнера Jackett нет в docker ps. Это не поломка — Lampac сам отдаёт Jackett-совместимый API.

curl -s "http://192.168.1.14:9118/lampainit.js" | grep -oE "Lampa.Storage.set\('[^']+', ?'[^']*'\)"
#   jackett_url        = '192.168.1.14:9118'   ← сам Lampac, не сторонний сервис
#   jackett_key        = '1'
#   parser_torrent_type= 'jackett'
#   parser_use         = 'true'
#   source             = 'cub'
#   torrents_use       = true      (lampa_settings)

⚠️ API живёт на КОРНЕ, без префикса /jackett:

curl -s "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix"
# ✅ 200 — реальные результаты: Tracker:rutor, MagnetUri, Size, Seeders, ffprobe
curl -s "http://192.168.1.14:9118/jackett/api/v2.0/indexers/all/results"   # ❌ 404

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

⚠️ Sonarr — второй источник автоскачки (не только Radarr)

Alex упомянул его не зря. Замер 2026-09-22:

SKEY=$(ssh kraken 'grep -oE "<ApiKey>[^<]+</ApiKey>" \
  /srv/...6194539b.../docker/sonarr/config/config.xml' | sed 's/<[^>]*>//g')
# → 15fbec32f1cb4b05b1c2b229ce27359b

curl -sL "http://localhost:8989/api/v3/series?apikey=$SKEY" | jq length        # → 13 сериалов
curl -sL "http://localhost:8989/api/v3/series?apikey=$SKEY" \
  | jq '[.[]|select(.monitored)]|length'                                       # → 13 monitored
curl -sL "http://localhost:8989/api/v3/queue?apikey=$SKEY" | jq .totalRecords  # → 19 в очереди
Сервис Объектов monitored Очередь
Radarr 50 фильмов 45 6
Sonarr 13 сериалов 13 19

Отключать надо оба — иначе Sonarr продолжит качать сериалы.

Transmission — замер

ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.json
#   download-dir /downloads · rpc-port 9091 · speed-limit-down-enabled false
# /config/torrents/*.torrent = 257 файлов (накопленный список РАЗДАЧ, не активная качка)
# CPU 0.76% — простаивает. NetIO 290G↑/26.7G↓ (суммарно за жизнь контейнера)

Полный состав контейнеров Кра́кена: lampac · radarr · jellyfin · transmission · portainer · flaresolverr · prowlarr · sonarr · watchtower · rclone · xray-reverse-bridge · hermes-kraken · cloudflared

Что осталось

  1. Лимит кэша TorrServer — сейчас дефолт (CacheSize 96 МБ, UseDisk: false). Диск 396 ГБ свободно, на HDD TorrServer не пишет вовсе — вопрос неактуален.
  2. Шаг 7 плана — watchlist в UI Lampa — ✅ серверная часть сделана 2026-09-22, 🔴 архитектура признана неверной, переделывается: см. personal/plans/lampac-watchlist-plugin, разделы «ПЕРЕДЕЛКА 2» и «✅ ПРОВЕРКА "как это реализуемо"».
  3. Проверка полосы на торрент-источнике (remux 60–80 Мбит) — не сделана.
  4. Постеры/карточка в каталоге — не сделаны, image пустой. Решение — брать из Radarr (images[].remoteUrl) или MediaCover/, см. раздел выше.
  5. Автоскачка Radarr + Sonarr — 45 monitored / очередь 6 у Radarr, 13 monitored / очередь 19 у Sonarr. План отключения на апруве (personal/plans/lampac-watchlist-plugin, «ПЕРЕДЕЛКА 2»).

Карточка фильма в модуле Catalog — рабочая схема (проверено 2026-09-22)

Модуль Catalog (в образе /lampac/module/Catalog/). Каталог watchlist: sites/watchlist.yaml + JSON в wwwroot/.

Списки

menu:
  - categories:
      Новинки: new      # ПЕРВОЙ — она игнорирует format, берёт list.uri
      Фильмы: films
      Мультфильмы: cartoons
      Аниме: anime
    format:
      "-": |
        return $"http://127.0.0.1:9118/watchlist-{cat}.json";
list:
  uri: watchlist-new.json   # ← сюда смотрит первая категория
content:
  nodes: "$.items[*]"
  href: { attribute: href }   # href = TMDB URL → id = "movie/NNN"

Карточка

CardController.Index(plugin, uri, type) — cat не принимает.

  • url = $"{init.host}/{uri}" — дефолт, даёт http://127.0.0.1:9118/movie/424781 → 500.
  • card_parse.initUrl исполняется как C# (CSharpEval, объект CatalogInitUrlCard(host, args, uri, query, type)) → можно собрать динамический URL.
  • card_parse.node — статичный JSONPath (json.SelectToken(node)), подстановок нет; / = вложенность.

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

card_parse:
  initUrl: |
    return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
  node: card
  name:          { attribute: name }
  original_name: { attribute: original }
  year:          { attribute: year }
  image:         { attribute: poster }
  description:   { attribute: overview }

Файл wwwroot/cards/card-movie-424781.json:

{ "card": { "name": "...", "title": "...", "original": "...", "original_title": "...",
            "year": "...", "rating": "...", "poster": "...", "img": "...",
            "overview": "...", "genres": "...", "cast": "...", "runtime": 116 } }

Ключи дублируются (name+title, poster+img): CardController отдаёт title/original_title, а card_parse ищет name/original.

Эндпоинты

Запрос Назначение
GET /catalog список каналов; main = категории
GET /catalog/list?plugin=watchlist&cat=films&page=1 элементы секции
GET /catalog/card?plugin=watchlist&uri=movie/424781 карточка

🔴 Не по /card — там 404 и ложный вывод «карточка сломана».

Файлы и права

  • Статика раздаётся из /lampac/wwwroot/ (core UseStaticFiles).
  • Владделец — 1000:1000 (пользователь lampac в контейнере), каталоги 755, файлы 644. Иначе Permission denied → 404.
  • macOS tar → AppleDouble ._*, добавлять --no-mac-metadata.

Деплой

python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
python3 deploy_lampac.py /tmp/wl-out --skip-plugin

Скрипты: /Users/admin/lampac-tmp/. Логика заливки — tar+scp+sudo, ssh -t для sudo (без tty sudo требует пароль), каждая команда отдельным вызовом (&&-цепочка рвёт tty).

Связанные

🔴 ПОЧЕМУ КАРТОЧКА = «ЗДЕСЬ ПУСТО» (найдено 2026-09-22, вечер)

Симптом: данные в карточку приходят (title, img, overview), но в UI «здесь пусто», нет кнопки воспроизведения.

Причина: plugin.js при клике:

if(json.tmdb_id){ status.need++; Lampa.Api.sources.cub.reactionsGet(
    {id: json.tmdb_id, method: json.original_name ? 'tv' : 'movie'}, ...); }

Без tmdb_id (числа, не строки!) Lampa считает карточку неполной → пустой экран.

Два бага в цепочке, оба надо было починить

  1. ModInit.cs:285 — nodeValue возвращает ЛЮБОЕ значение как строку.

    string value = valueToken switch {
        JValue jValue => jValue.Value?.ToString(),   // ← число → "424781"
        ... };
    

    Поэтому tmdb_id из JSON приходил строкой, и if(json.tmdb_id) давала не число.

  2. Кэш в памяти процесса переживает docker restart. hybridCache ключ catalog:card:{plugin}:{uri} живёт в памяти. При правке данных старые ответы продолжают отдаваться. Обязательно docker restart lampac и проверять на НЕзакэшированном uri.

Фикс — format в card_args

format исполняется как C# (CSharpEval, объект CatalogNodeValue(value, host)) и может вернуть настоящий тип, а не строку:

card_args:
  - name_arg: tmdb_id
    attribute: tmdb_id
    format: "long.TryParse(value, out long v) ? v : 0"
  - name_arg: source
    attribute: media_type
  - name_arg: runtime
    attribute: runtime
    format: "long.TryParse(value, out long v) ? v : 0"
  - name_arg: vote_average
    attribute: vote_average
    format: "double.TryParse(value, out double v) ? v : 0"
  - name_arg: rating
    attribute: rating        # строкой — Lampa показывает в бейдже

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

{ "id": "movie/424781", "title": "Простите за беспокойство",
  "original_title": "Простите за беспокойство", "tmdb_id": 424781,
  "source": "movie", "runtime": 111, "rating": "6.8", "vote_average": 68,
  "genres": [{"name":"фэнтези"},{"name":"фантастика"},{"name":"комедия"}],
  "overview": "...", "img": "http://192.168.1.14:9118/proxyimg:210:0/..." }

Результат: 352 из 352 карточек отдают числовой tmdb_id.

Про CUB — почему «только фильмы из списка»

CUB и «Мой список» — два независимых источника Lampa, одновременно показать нельзя. Переключение: Настройки → Остальное → Основной источник. Пункта «Источник» в настройках НЕ существует.

Питфолл проверки на ТВ

Lampa на ТВ кэширует манифест и ответы. После правок надо полностью выгрузить приложение (не свернуть), иначе клиент держит старый кэш.


🔴 Анатомия CUB-источника — что нужно, чтобы карточка открылась (разобрано 2026-09-22)

Разбор app.min.js (2.1 МБ; Лампа грузится с сервера, файл живёт в /lampac/wwwroot/lampa-main/app.min.js). Копия локально:

ssh kraken "sudo docker cp lampac:/lampac/wwwroot/lampa-main/app.min.js /tmp/app-min.js"
scp -q kraken:/tmp/app-min.js /tmp/app-min.js

Источники регистрируются как sources = { tmdb: TMDB, cub: CUB }

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

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

function full$1(params, oncomplite, onerror) {
  var status = new Status(9);
  if (Utils$1.dcma(params.method, params.id)) return onerror();
  get$7('3/' + params.method + '/' + params.id
        + '?api_key=' + TMDB$1.key()
        + '&append_to_response=content_ratings,release_dates,external_ids,'
        + 'keywords,alternative_titles,images'
        + '&language=' + Storage.field('tmdb_lang'), params, function (json) {
    if (json.status_code) return status.stop(), onerror();
    json.source = 'cub';          // ← 🔴 CUB проставляет source САМ
    ...
    status.append('movie', json);
  }, ...);
}

Вывод: json.source — обязательное поле. CUB ставит его сам; наш плагин делает то же (plugin.js:42: json.source = params.source).

Как Explorer открывает карточку

Explorer$1 (строка ~39403) читает this.object.movie || this.object.card || {}. Данные приезжают из Activity.push({ component: 'full', id, method, card, source }).

Три места, где пушится full:

Строка Контекст source
44291 кнопка «Смотреть» в фиде element.data.source || 'tmdb'
40247 кнопка «Назад» params.movie.source
18668 уведомление element.card.source || (ru/uk/be ? 'cub' : '')

🔴 Фолбэк || 'tmdb' — если у элемента списка нет поля source, Lampa уйдёт в tmdb, а не в наш каталог. Проверить, добавляет ли плагин source в элементы catalog/list (в ответе /catalog/list этого поля нет).

Что читает full из ответа карточки (plugin.js:42-43)

json.source = params.source;
if (json.seasons && json.seasons.length) { ...tv/{tmdb_id}/season/{N}... }
if (json.tmdb_id) { ... cub.reactionsGet({id: json.tmdb_id,
                     method: json.original_name ? 'tv' : 'movie'}) ... }
if (json.credits)         status.data['persons']  = json.credits;
if (json.recommendations) status.data['recomend'] = addSource(...);
if (json.similar)         status.data['simular']  = addSource(...);
if (json.videos)          status.data['videos']   = addSource(...);
status.append('movie', json);

→ Обязательные поля: tmdb_id (число), original_name или original_title (определяет tv/movie), img, overview. seasons/credits/recommendations/similar/videos — опциональны.

ТВ-диагностика при «здесь пусто»

Что Как проверить
Есть ли соединения ТВ → Lampac ssh kraken "sudo ss -tn | grep 9118"
Ноль соединений Lampa на ТВ не общается с Lampac — не запущена либо источник не активен
Захват запросов ТВ ssh kraken "sudo timeout 40 tcpdump -i any -n -A 'host 192.168.1.70 and port 9118'"
tcpdump на Кра́кене не был установлен → поставлен sudo apt-get install -y tcpdump (4.99.3)
Front app на ТВ script -q -c "luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}" /tmp/fg.txt → вернул com.lampac.tv
lampac_url на ТВ loader.js: localStorage['lampac_url'], дефолт http://192.168.1.14:9118 ✅
Кэш манифеста loader.js: cache_version = floor(Date.now()/9e5) — смена сервера в течение ~15 мин не подхватится
Служба devtools com.webos.service.devtools НЕ существует на webOS 5.4.2 Lite

✅ РЕШЕНО: partNext — рабочий механизм (проверено 2026-09-22, сессия 8)

Требование Alex: секции Watchlist ВНУТРИ источника CUB, без ручного переключения. Переключение источника — не решение (отвергнуто дважды).

Почему провалилась обёртка cub.main (3 итерации)

Родной main$1() в app.min.js объявлен внутри замыкания Lampa и зависит от локальных permit, get$7, Lang, permit.child_small, Helper$b:

function main$1() {
  var parts_limit = 6;
  var parts_data = [
    function (call) { get$7('?sort=now_playing', params, function (json) {
        json.title = Lang.translate('title_now_watch'); call(json); }, call, {life: day*2}); },
    function (call) { if (permit.child_small) return call(); ... }
  ];
  ...
}
Симптом Замер CDP на ТВ Корень
cub.main({}) → вечный pending, 0 fetch {"pending":true} через 15 с permit = undefined вне замыкания → падение на первой проверке → callback не зовётся
секции не добавлялись, ошибок нет cubMainSrc = ...var wl = Manifest.catalogs['watchlist']... Manifest/Utils/network = undefined вне замыкания → ReferenceError → catch → родной фид
«Нет подключения к сети» в логе ТВ {localhost}/catalog?uid=...&token={token} Utils.account() без Manifest.api_host подставляет {localhost}

🔴 Вывод: cub.main НЕВЫЗЫВАЕМ извне by design. Обёртка — тупик, не «недоработка».

✅ Правильный механизм: перехват Lampa.Api.partNext

Родной main$1() собирает parts_data — массив функций-секций — и отдаёт его в глобальную Lampa.Api.partNext(parts, parts_limit, partLoaded, partEmpty):

function partNext(parts, parts_limit, partLoaded, partEmpty) {
  var pieces = parts.filter(function (p) { return typeof p == 'function'; })
                     .slice(0, 0 + parts_limit);
  if (pieces.length) {
    new Progress().append(pieces).start(function (result) {
      var data = result.filter(function (r) { return r && r.results && r.results.length; });
      ...
    });
  } else partEmpty();
}

Патч добавляет наши функции-секции в parts до вызова оригинала:

Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
    if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
        var menu = window.__wl_menu;
        if (menu && Object.keys(menu).length) {
            parts.__wl_done = true;
            Object.keys(menu).forEach(function (name) {
                parts.push(wlSection(name, menu[name]));
            });
        }
    }
    return orig.apply(this, arguments);
};

Секция — функция call(json), те же контракты, что у родных CUB:

function wlSection(name, url) {
    return function (call) {
        wlGet(url, function (json) {
            json.title = name;
            json.source = 'watchlist';        // ← обязательно, иначе карточка уйдёт в CUB/tmdb
            if (Lampa.Arrays.isArray(json.results))
                json.results.forEach(function (it) { it.source = 'watchlist'; });
            call(json);
        }, function () { call(); });          // ошибка → пустой вызов, пул не ломается
    };
}

Проверено на живом ТВ (CDP): pnCalls: 1, lastPartsCount: 28 — CUB собрал главную из 28 функций (24 родные + наши 4). Activity main, patched: true, меню ["Новинки","Фильмы","Мультфильмы","Аниме"]. В DOM рендерились: «Рассекая волны», «Хамнет», «Эхопраксия», «WeWork создание и крах компании».

🔴 В замыкании доступны ТОЛЬКО глобальные объекты

Нужно ❌ Не работает в этом контексте ✅ Работает
api_host Manifest.api_host Lampa.Manifest.api_host (фолбэк — хардкод http://192.168.1.14:9118)
URL-склейка Utils.account(url) → {localhost} ручная: wlAbs(url) = host + path
HTTP network.silent(...) new Lampa.Reguest().silent(absURL, ok, fail), фолбэк fetch
список секций Manifest.catalogs['watchlist'].menu window.__wl_menu (заполняется в loadCatalogs из /catalog)
function wlAbs(url) {
    if (!url) return '';
    if (/^https?:\/\//i.test(url)) return url;
    return wlHost() + (url.charAt(0) === '/' ? url : '/' + url);
}

🔴 Lampa.Reguest.silent(абсолютныйURL) работает идеально (замер: 33 фильма, XHR уходит корректный). Проблема была только в относительных URL через Utils.account.

Сборщик build_plugin.py (заменил patch_cub_inject.py)

cd /Users/admin/lampac-tmp
python3 build_plugin.py     # plugin-patched.js + INJECT → plugin-cub.js
node --check plugin-cub.js  # обязательно
node -e "const s=require('fs').readFileSync('plugin-cub.js','utf8');let b=0;for(const c of s){if(c=='{')b++;if(c=='}')b--;}console.log('баланс:',b)"  # → 0

Детерминирован: [до startPlugin] + INJECT + [новый startPlugin] + '})();'. Инварианты: WL-INJECT-START=1, function wlPatchPartNext=1, function startPlugin=1, network.silent в INJECT=0. Итог: 15476 Б (в отдаваемой сборке 36956 Б).

🔴 Питфолл прошлого сборщика: проверки "X" not in s шли по уже модифицированной строке → повторный запуск вставлял INJECT дважды → лишняя } → SyntaxError, баланс скобок {=123, }=122. Всегда node --check + баланс ПЕРЕД заливкой.

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

scp plugin-cub.js kraken:/srv/.../lampac/catalog/plugin.js
ssh kraken 'P=/srv/.../lampac/catalog; chmod 644 $P/plugin.js; chown 1000:1000 $P/plugin.js; rm -f $P/._*; docker restart lampac'
# проверка отдаваемой сборки (Staticache!):
curl -s http://192.168.1.14:9118/catalog.js -o /tmp/cj.js
grep -c 'WL-INJECT-START' /tmp/cj.js   # → 1
grep -c 'wlPatchPartNext' /tmp/cj.js   # → 3

⚠️ ТВ держит CMY старый плагин в памяти. После заливки обязателен перезапуск приложения на ТВ. Признак незакрытого перезапуска: String(Lampa.Api.sources.cub.main) содержит старую обёртку wlEnsureMenu, а Lampa.Api.__wl_partnext = true — две версии одновременно, старая ломает главную (uid/token, вечный pending).

⚠️ Перезапуск Lampa на ТВ — luna-send НЕ РАБОТАЕТ на этой прошивке

Проверено 2026-09-22: luna-send по ssh возвращает пустоту (не help, не ошибку), и через файл (sh /tmp/tv_restart.sh), и с reload:true, и через close+launch. Страница devtools не меняется (reset= в URL тот же) → приложение не перезапускается.

Рабочие альтернативы (не проверены до конца, следующая сессия):

  • Page.reload + Network.clearBrowserCache через CDP (порт 9998)
  • пульт: выйти из приложения и открыть заново

Питфолл: eval(pluginCode) внутри IIFE не создаёт глобальных функций

После eval(code) функции wlHost()/wlAbs() снаружи не видны (ReferenceError) — они в замыкании IIFE. Это норма: cub.main/partNext их видят (одно замыкание), внешние CDP-пробы — нет. Проверять только через Lampa.Api.main() либо чтение DOM.

Питфолл: cub.list без host → 404

Lampa.Api.sources.cub.list({url:'/catalog/list?...'}) → HTTP 404 (относительный URL). Прямой fetch с абсолютным URL → 200, 9496 байт. Ещё одно подтверждение того, что в этом контексте нужны только абсолютные URL.

🔴 ТРЕБОВАНИЕ Alex: секции ВНУТРИ CUB, БЕЗ ручного переключения

Дословно: «надо блядь добавить в CUB список новые секции а не ручное блядь переключение!»

Текущий plugin.js подменяет разделы через:

Lampa.Listener.follow('menu', function (e) {
  if (e.type=='action' && (e.action=='tv'||'cartoon'||'anime'||'movie'||'relise')) {
    var catalog = Api.getCatalog();
    if (!catalog) return;          // ← 🔴 молчит, если активен НЕ наш источник
    ...
    e.abort();
  }
});

Почему не работает для CUB: Api.getCatalog() читает Lampa.Manifest.catalogs[<текущий источник>]. При выбранном источнике CUB нашего каталога там нет → return → открывается родной раздел CUB.

Что делать вместо этого (задача следующей сессии): дорисовывать секции в корень CUB — не перехватывать menu, а патчить рендер главной (строки ~44260-44310: feed / category_full) либо добавлять ряды через Lampa.Listener.follow('activity'). Цель: Alex открывает обычный CUB и сверху видит наши секции с реальными элементами (постеры, рейтинги) — как в исходной формулировке из personal/plans/lampac-watchlist-plugin.

🔴🔴 ГЛАВНАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО»: кириллица в ключе каталога (2026-09-22)

Это была настоящая причина. Не данные, не формат, не кэш — ключ каталога.

Как найдено

tcpdump на Кра́кене, фильтр по ТВ (192.168.1.70), клик по фильму в UI:

GET /catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=movie%2F1280015&type=movie HTTP/1.1
HTTP/1.1 400 Bad Request

plugin=Мой список — URL-encoded кириллица. Воспроизведено curl'ом:

curl "http://192.168.1.14:9118/catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=movie%2F424781&type=movie"
# → "init not found", HTTP 400

Механика (две строки в ModInit.cs + одна в ApiController.cs)

ApiController.cs:136 — ключ каталога в /catalog:

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

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

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

site = site.ToLowerAndTrim();
site = Regex.Replace(site, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase);

🔴 Регулярка вырезает ВСЮ кириллицу целиком. «Мой список» → "". Дальше: File.Exists("/lampac/module/Catalog/sites/.yaml") = false → FindSiteByDisplayName("") = пусто (сравнивает с уже очищенной строкой) → return null → CardController: BadRequest("init not found") → HTTP 400 → в Lampa «Здесь пусто».

Ключевой вывод: fallback FindSiteByDisplayName никогда не сработает для кириллического displayname — regex убивает строку ДО сравнения. Это не лечится названием файла, только явным plugin в латинице.

ФИКС — явный plugin в YAML

enable: true
jsonPath: true
displayindex: 1
displayname: Мой список          # ← остаётся, это НАДПИСЬ в UI
plugin: watchlist                # ← 🔴 ОБЯЗАТЕЛЬНО, латиница = ключ каталога
host: http://127.0.0.1:9118

init.plugin переопределяет ключ (ApiController.cs:136), и ТВ зовёт plugin=watchlist → файл sites/watchlist.yaml находится штатно.

⚠️ Побочный эффект (ожидаемый): ключ в /catalog меняется Мой список → watchlist. Надпись на ТВ не меняется, только внутренний идентификатор.

Проверка

Ключ Запрос Ответ
Мой список (было) plugin=%D0%9C%D0%BE%D0%B9... 400 init not found
watchlist (стало) plugin=watchlist 200, tmdb_id: 424781 числом, source: "movie"

🔴 ПРАВИЛО ДИАГНОСТИКИ (стоило часа)

Проверять карточку curl'ом надо ТЕМ ЖЕ ключом, который шлёт ТВ. Я подставлял plugin=watchlist руками и получал 200, а ТВ слал plugin=Мой список и получал 400 — и это выглядело как «сервер всё отдаёт, проблема в клиенте». Единственный надёжный способ узнать реальный запрос — tcpdump на сервере во время клика:

ssh kraken "sudo timeout 40 tcpdump -i any -n -A 'host 192.168.1.70 and port 9118' 2>/dev/null | grep -aE 'GET /|HTTP/1'"

Права, tar и macOS-мусор при заливке cards/

Три отдельных питфолла, каждый даёт молчаливый 404 на статике:

  1. macOS tar кладёт AppleDouble-файлы ._* в архив → мусор в wwwroot. Фикс: tar --no-mac-metadata + sudo rm -f .../._*.
  2. Права. Если файлы приехали root:root 600 — dotnet отдаёт Permission denied, статика отвечает 404 (не 403!), и это выглядит как «файла нет». Фикс: chown -R 1000:1000 (пользователь lampac в контейнере) + chmod -R 755, затем chmod -R a+rX.
  3. find -exec через ssh -t рвётся — find: missing argument to '-exec' (tty съедает {} \;). Фикс: два отдельных chmod -R вместо find -exec.

⚠️ Также: &&-цепочка в ssh -t рвёт tty-сессию («Connection closed»). Каждую команду — отдельным вызовом.

🔴 Staticache — /catalog.js собирается и КЭШИРУЕТСЯ

/catalog.js — не файл, а сборка: Lampac берёт plugin.js из /lampac/module/Catalog/plugin.js, подставляет api_host и catalogs (отсюда ~18 КБ разницы от исходника) и кэширует результат через атрибут [Staticache].

Питфолл, который стоил времени: залил новый plugin.js (14099 Б, патч на месте — grep подтверждает), а /catalog.js продолжал отдавать старую сборку (32205 Б, патча нет). Выглядело как «патч не применяется».

=== патч в контейнере:
14099 /lampac/module/Catalog/plugin.js     ← патч ЕСТЬ
=== что отдаёт клиенту:
32205 байт, injectWatchlistParts: 0        ← патча НЕТ

Лечение: docker restart lampac (сбрасывает Staticache-кэш), затем проверять размер и маркеры в отдаваемом файле, а не только в файле на диске:

curl -s "http://192.168.1.14:9118/catalog.js" -o /tmp/cj.js
wc -c < /tmp/cj.js                  # размер сборки
grep -c injectWatchlistParts /tmp/cj.js   # наш маркер
grep -c patchCubMain /tmp/cj.js

⚠️ Заголовки при этом отдают Cache-Control: no-cache — но это HTTP-кэш клиента, а Staticache кэширует на сервере, поэтому заголовки не помогают и клиентская очистка кэша проблему не решает.

Внедрение секций Watchlist в CUB-фид (plugin-cub.js)

Требование Alex: секции Watchlist должны быть видны ВНУТРИ источника CUB, без ручного переключения «Основной источник». Переключение — не решение.

Почему штатный хук не подходит

plugin.js содержит Lampa.Listener.follow('menu', ...) + e.abort(): он перехватывает и ОТМЕНЯЕТ разделы movie/tv/cartoon/anime/relise, подменяя их нашими. Но Api.getCatalog() читает Manifest.catalogs[Lampa.Storage.field('source')] — то есть каталог текущего источника. При активном CUB нашего каталога там нет → хук молчит → открывается родной CUB. Подмена работает только когда активен наш источник.

Рабочая механика: обернуть main CUB-источника

main$1() в app.min.js:33880 собирает массив parts_data — функций-загрузчиков, каждая добавляет секцию на главную, и отдаёт через Lampa.Api.partNext. Значит наши секции надо добавить в этот массив, а не подменять раздел.

Патч (patch_cub_inject.py → plugin-cub.js):

function injectWatchlistParts(original) {
    return function (params, oncomplite, onerror) {
        original(params, function (cub_data) {
            var wl = Manifest.catalogs['watchlist'];
            if (!wl) return oncomplite(cub_data);
            var menu = wl.menu || {}, keys = Object.keys(menu);
            var list = Lampa.Arrays.isArray(cub_data) ? cub_data.slice() : [cub_data];
            var pending = keys.length;
            keys.forEach(function (name) {
                network.silent(Utils.account(menu[name]), function (json) {
                    json.title  = name;              // заголовок секции
                    json.source = 'watchlist';       // источник для карточек
                    (json.results || []).forEach(function (it) { it.source = 'watchlist'; });
                    list.push(json);                 // ДОБАВЛЯЕМ, не заменяем
                    if (--pending === 0) oncomplite(list);
                }, function () { if (--pending === 0) oncomplite(list); });
            });
        }, onerror);
    };
}
function patchCubMain() {
    var src = Lampa.Api.sources.cub;
    if (!src || src.__wl_injected) return;
    src.main = injectWatchlistParts(src.main);
    src.__wl_injected = true;
}

Вызов — по событию app:ready, потому что нужны и Lampa.Api.sources.cub, и загруженный Manifest.catalogs['watchlist']:

if(window.appready){patchCubMain();}
else{Lampa.Listener.follow('app',function(e){if(e.type=='ready')patchCubMain();});}

🔴 Ключевое в карточках наших секций — source: 'watchlist'. Без него клик уйдёт в CUB-карточку по TMDB-id и «Мой список» потеряется.

Питфолл скрипта-патчера

🔴 Порядок проверок в patch_cub_inject.py критичен. Если сначала вставить INJECT (который содержит текст patchCubMain), то последующая проверка "patchCubMain()" not in s даст ложное «уже есть» и вызов не вставится. Правильно: сначала проверить/вставить ВЫЗОВ (if(window.appready){patchCubMain();}), и только потом — определение INJECT.

Проверка синтаксиса обязательна: node --check plugin-cub.js.

Файлы патча

Файл Что
plugin.js оригинал из образа (10670 Б)
plugin-patched.js + enumerable:true (видимость источника в UI)
plugin-cub.js + инжект секций в CUB-фид (финальный, заливается)
patch_plugin.py делает plugin-patched.js
patch_cub_inject.py делает plugin-cub.js из plugin-patched.js

deploy_lampac.py шаг 4 берёт plugin-cub.js, с фолбэком на plugin-patched.js.

Итоговое состояние (2026-09-22, конец сессии)

Что Статус
Ключ каталога plugin: watchlist (латиница) ✅
/catalog watchlist первым, main = Новинки/Фильмы/Мультфильмы/Аниме
Карточки 352 из 352 отдают числовой tmdb_id ✅
Списки films 360 (постеров 308), new 33 (30), cartoons 3 (3), anime 7 (7)
tmdb_id в карточке числом через format: "long.TryParse(value, out long v) ? v : 0"
runtime, vote_average тем же способом через format
displayname «Мой список» — надпись в UI
✅ Alex подтвердил в UI фильмы Watchlist открываются и ПРОИГРЫВАЮТСЯ
Инжект секций в CUB-фид ✅ механизм найден и проверен (partNext), залито (36956 Б в сборке)
Причина «секции не появились» 🔴 обёртка cub.main невызываема (permit в замыкании) → перешли на partNext
Осталось перезапуск Lampa на ТВ (luna-send не работает — нужен пульт или CDP Page.reload)
Сборщик плагина build_plugin.py (детерминированный, с инвариантами); patch_cub_inject.py отменён

Скрипты: /Users/admin/lampac-tmp/gen_watchlist_v2.py, deploy_lampac.py (--skip-plugin для быстрого прогона), build_plugin.py, deploy_cub.py, restart_lampa.sh, probe_tv.py, probe_cub_main.py. Проверка в шаге 7 валидирует именно ключ watchlist и предупреждает, если в /catalog остался русский ключ.

🔴 Полная инвентаризация диска Кра́кена (2026-09-22, вечер)

Alex: «проверь все что скачано сейчас из сериалов и фильмов (не только в sonarr/radarr папках) составь табличку что из этого есть в списке watchlist (и соответственно доступно в lampa) и то чего нет в lampa» → «вообще все. нужна полная инвентаризация что можно сносить, а что нельзя».

Отдельный план с таблицей: personal/plans/kraken-media-inventory-cleanup. Здесь — только методика и переиспользуемые факты.

Цифры

802 видеофайла · 233 позиции · 1236.1 ГБ занято
  A. в watchlist (Lampa покажет):  29 поз. / 456.0 ГБ
  B. не в watchlist:              204 поз. / 780.1 ГБ
  точных дублей (имя+размер):      46 групп / 89.2 ГБ зря
Папка Файлов ГБ Индексируется arr
movies 51 309.7 ❌
documentaries-series 72 235.6 ❌
movies-radarr 11 176.2 ✅
cartoons 102 139.8 ❌
series-sonarr 57 116.1 ✅
cartoons-series 433 104.6 ❌
downloads 45 78.6 ❌ staging
documentaries 25 71.9 ❌
series 6 3.6 ❌

🔴 Radarr/Sonarr индексируют только 252 ГБ из 1236 (21%). Остальные ~984 ГБ — ручные закачки, невидимые в UI стека.

🔴 downloads/complete — раздачи не удаляются после импорта

downloads/complete/sonarr/ держит завершённые раздачи, которые Sonarr уже скопировал в библиотеку и не убрал. Дубли подтверждены совпадением имени файла И размера байт-в-байт:

downloads/complete/sonarr/Очень странные дела.S03.WEB-DL.1080.HDR   21.33 ГБ  -> series-sonarr S03
downloads/complete/sonarr/Our.Flag.Means.Death.S01.2022...          19.31 ГБ  -> series-sonarr S01
downloads/complete/sonarr/Our.Flag.Means.Death.S02.WEBDL...         13.81 ГБ  -> series-sonarr S02
downloads/complete/sonarr/Stranger Things.s04.WEB-DL(HEVC)1080p.xz  13.06 ГБ  -> series-sonarr S04
downloads/complete/sonarr/Kiberderevnya.S01.2023.WEB-DL.1080p       11.11 ГБ  -> The Cyberfarm S01

= 78.6 ГБ чистого дубля. Sonarr настроен «копировать», а не «перемещать» (или Remove Completed выключен) — отсюда двойной расход.

⚠️ Life.Is.Beautiful.1997.BDRip.1080p.mkv (10.62 ГБ) лежит трижды: movies-radarr/Life Is Beautiful (1997)/ + cartoons/life is beautiful/ + ещё копия. Фильм в cartoons — ошибка раскладки, не случайность импорта.

⚠️ Ложный дубль: Stranger Things S03

downloads/.../Очень странные дела.S03 и series-sonarr/Stranger Things/Season 3 совпадают байт-в-байт, но в series-sonarr нет S01 (только S02–S04) — библиотека формировалась из этой же раздачи. Перед удалением downloads-копии проверить, что Sonarr видит S03 как присутствующий.

Метод: как группировать файлы в «позиции»

Плоский find даёт 802 строки — бесполезно. Группировка:

# movies / cartoons / documentaries — файлы в корне = каждая своя позиция
#   (иначе единственный «Вивариум» 1.46 ГБ склеивается в группу на 135 ГБ)
if top in ("movies", "cartoons", "documentaries"):
    return (top, dirs[0]) if dirs else (top, r["file"])
# downloads/complete/sonarr/<Pack Name>/... — по полному пути пака (сезонный пак)
if top == "downloads" and len(dirs) >= 3 and dirs[0] == "complete":
    return (top, "/".join(dirs[:3]))
# сериалы — первый подкаталог = шоу
return (top, dirs[0]) if dirs else (top, r["file"])

🔴 Матч с watchlist — строгое равенство НЕДОСТАТОЧНО (исправлено 2026-09-22)

Подстрочный матч (c in e["name"]) даёт мусор: короткие нормализованные строки вроде гладиатор совпадают со множеством позиций, счётчик «в watchlist» раздувается до неверного. Порядок эффекта: наивный подстрочный матч → 42 позиции / 811 ГБ; строгий → 29 позиций / 456 ГБ.

⚠️ Но и строгое равенство по русскому name — тоже неверно, и это отдельная ошибка. Первый прогон сравнивал имя файла только с русским name watchlist-позиции. Файлы, названные по оригиналу, не находились. Alex поймал это на трёх тайтлах:

Файл на диске В watchlist Русское name
Bram.Stoker's Dracula.1992 ✅ TMDB 6114 «Дракула»
The.Crow.1994 ✅ TMDB 9495 «Ворон»
despicable.me.4.2024 ✅ TMDB 519182 «Гадкий я 4»
Вивариум_2019_BDRip ✅ TMDB 458305 «Вивариум»

🔴 Матчить надо против ОБОИХ полей — name И original — и с проверкой года. Структура позиции watchlist:

{"name": "Ворон", "original": "The Crow", "year": 1994,
 "href": "https://www.themoviedb.org/movie/9495"}

Рабочее правило: извлечь латинскую фразу из имени файла (latin_phrase — длиннейший прогон [a-z0-9']-слов после нормализации) и сравнить с original; отдельно сравнить нормализованное имя с name. Обязательно сверять год ±1 — иначе inside out (2015) ложно матчится с «Головоломка 2» (2024): разные фильмы. В watchlist один и тот же русский name может стоять у трёх разных фильмов («Дракула» = 1992 / 2014 / 2025) — год единственный различитель.

⚠️ Токенный матч по пересечению слов тоже гадит и как метод не годится: «пингвины. шпион в толпе» → «Шпион» (Spy, 2015), «the secret life of elephants» → «Список заветных желаний». Использовать только фразовое равенство + год.

Нормализация (обе стороны сравнения, обязательна): снять расширение → [...] → (...) → год → не-буквы → шум-слова (качество, релиз-группа, web-dl, bdrip, x264, megapeer, bluebird, hdrezka, транслит-хвосты) → сжать пробелы. Для сериалов дополнительно strip_ep: S01E04, N серия, N выпуск, сезон N.

⚠️ Год из имени файла снимается при нормализации — его надо вытаскивать ОТДЕЛЬНО (year_of()) из исходной строки, иначе сверять нечего.

🔴 Цифры «29 / 456 ГБ» в этом документе и в плане ЗАНИЖЕНЫ — матч по одному русскому name пропустил минимум 4 позиции (27.3 ГБ), которые на самом деле в watchlist. Пересчёт по правилу «name + original + год ±1» — следующий шаг, ждёт от Alex решения по допуску года.

🔴 TMDB-ID в watchlist.json отдельным полем НЕ лежит. Он в href: https://www.themoviedb.org/movie/424781 → re.search(r"/(\d{2,9})/?$", href). Попытка читать item["tmdb_id"] даёт null по всем 376 позициям.

missing.json не пересекается с диском

Все 30 позиций из missing.json (маркер ❓) — рукописные заметки в списке (Солярис Тарковский, Идиот Бортко, Замок Балабанов, Спрут сериал), а не скачанные файлы. Совпадений с диском — 0. Вывод: «недоступно в Lampa» для файлов на Кра́кене вызывается отсутствием в watchlist, а не отсутствием в TMDB. Это разные причины, не смешивать.

Где что лежит

Диск:  /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/media/
       (49e8f586 — symlink на него; отдельного диска нет, проверено docker inspect)

Артефакты инвентаризации на Mac:
  /Users/admin/kraken-inventory.json   — 233 позиции, полные поля
  /Users/admin/kraken-inventory.csv    — таблица для Excel
  /Users/admin/kraken-dupes.json       — 46 групп дублей
  /Users/admin/kraken-table-AB.md      — markdown A/B таблицы

Скрипты инвентаризации: /tmp/kraken_inventory3.py (финальный), /tmp/kraken_final.py (дубли + сводка), /tmp/kraken_break.py (разбор папки).

Кандидаты на удаление (по риск-уровню)

  1. downloads/complete (78.6 ГБ) — нулевой риск, всё есть в библиотеке.
  2. Дубли Life Is Beautiful — оставить одну из трёх копий.
  3. documentaries* вне watchlist (307 ГБ) — доксериалы, нужен просмотр глазами.
  4. Всё не из watchlist (780 ГБ) — включает Oldboy, Project Hail Mary, The Cyberfarm, Brassic — возможно нужное.

⚠️ Список «не в watchlist» требует пересчёта перед удалением. Матч по одному русскому name пропустил минимум 4 позиции (27.3 ГБ), которые в watchlist ЕСТЬ: Bram.Stoker's Dracula.1992 (18.04 ГБ), The.Crow.1994 (17.45 ГБ), despicable.me.4.2024 (2.18 ГБ), Вивариум_2019 (1.46 ГБ). Эти 4 — сносить НЕЛЬЗЯ, в текущем списке они помечены ошибочно. Oldboy и Project Hail Mary проверены отдельно поиском по watchlist.json — их там правда нет (0 совпадений по «Oldboy», 0 по «Hail Mary»); они в Radarr, но не в watchlist.

🔴 Transmission на Кра́кене — чистка перед удалением медиа (2026-09-22)

Состояние: 249 раздач, суммарно 1,92 ТБ заявленного размера.

Доступ к RPC

URL:  http://127.0.0.1:9091/transmission/rpc
Auth: НЕ требуется — settings.json: "rpc-authentication-required": false
      "rpc-username": "" (пусто)

🔴 Transmission требует session-id. Первый POST возвращает 409 и заголовок X-Transmission-Session-Id; забрать его и повторить POST с ним:

TS="http://127.0.0.1:9091/transmission/rpc"
SID=$(curl -s -i -X POST "$TS" -H 'Content-Type: application/json' \
  -d '{"method":"session-get"}' | tr -d '\r' | awk '/^X-Transmission-Session-Id:/{print $2}')
curl -s -X POST "$TS" -H "X-Transmission-Session-Id: $SID" \
  -H 'Content-Type: application/json' \
  -d '{"method":"torrent-get","arguments":{"fields":["id","name","status","percentDone",
      "totalSize","downloadDir","error","errorString"]}}'

Классификация раздач

error (число): 0 = нет ошибки, 3 = TR_STAT_LOCAL_ERROR → "No data found!". status: 0=stopped, 1=check-wait, 2=checking, 3=dl-wait, 4=downloading, 5=seed-wait, 6=seeding.

Кат. Раздач ГБ Признак
A. data missing 21 561,6 errorString содержит No data found
B. no space 8 88,8 errorString содержит Unable to save resume file: No space left on device
C. в списке на снос 204 1005,6 downloadDir под удаляемые папки
E. прочее 16 —

🔴 Категория A = «мёртвый вес»: файлы удалены руками, раздачи остались. Крупняк: Babylon 92,01 · Schindler's List 89,35 · Midsommar 85,59 · Triangle of Sadness 79,68 · Chaos Walking 58,36 · Oldeuboi 28,65 · Начало 24,25 · Whiplash 23,66 · Леон 19,73 ГБ. Плюс 6 раздач без файлов в downloads/complete/radarr и downloads/radarr (9–98 ГБ). Удаление из списка — без потерь.

⚠️ Категория C шире, чем список сноса. В неё попадают раздачи, чьи файлы остаются на диске: Шпион 2015 (25,9) · Секс Education (25,8) · Лука 2021 (22,4) · Сияние 1980 (40,4) · Терминал 2004 (17,7) · Хорошие парни (16,4). Удаление раздачи ≠ удаление файла: файл остаётся, сидирование прекращается.

⚠️ downloadDir раздач — это /media/..., а не host-путь. Внутри контейнера монтируется /media, host-путь другой. Матчить по префиксу /media/..., не по /srv/....

🔴 TorrServer/Lampa не используют media/ — играют с торрентов

curl -X POST http://127.0.0.1:9118/ts/settings -d '{"action":"get"}':

{"CacheSize": 100663296, "UseDisk": false, "TorrentsSavePath": "",
 "RemoveCacheOnDrop": false, "ReaderReadAHead": 83}

UseDisk: false + пустой TorrentsSavePath + кэш 96 МБ (только RAM) — TorrServer на диск не пишет, Lampa стримит раздачу напрямую из сети. Файлы в media/ на Кра́кене в воспроизведении Lampa не участвуют.

→ Снос watchlist-фильмов с диска не ломает Lampa. Ранее сделанный вывод «снесёшь то, что играет в Lampa» — НЕВЕРЕН (Alex: «она же не с кракена играет! ... она с торрентов играет»).

⚠️ Но Jellyfin (:8096, живёт на Кра́кене) может смотреть в media/. Перед удалением проверить его библиотеки; после — обязателен rescan.

Артефакты: /tmp/tr-list.json, /Users/admin/transmission-classified.json, /Users/admin/transmission-remove-plan.json Скрипты: /tmp/tr_diag.sh, /tmp/tr_classify.py, /tmp/step2_plan.py

✅ Решение Alex по чистке раздач (2026-09-22)

«плюс недакачанные если в transmission есть останавливай и тоже сноси. C - целиком»

data_missing (error==3)                  -> удалить из списка
downloadDir под удаляемые папки (C)      -> удалить ЦЕЛИКОМ (не только совпадающие)
status==0 (stopped) и percentDone < 1.0  -> удалить (недокачанные)

Итог плана: снести 240 раздач (1 769 ГБ), оставить 9 (20 ГБ). Остаются только документалки вне списка сноса (Sky.Sharks, Way of the Cheetah, Thailands.Wild.Cats, Jade.Eyed.Leopard, Frozen Kingdom of Snow Leopard, Russias.Wild.Tiger, Real Black Panther, Тайная жизнь собак, King Cobra) — все в /media/documentaries*, 100%, сидируются. ID: /tmp/tr-remove-ids.txt (240 id через запятую).

⚠️ После docker stop transmission RPC недоступен. Править состояние — через файлы bind-mount'а /srv/.../docker/transmission/ (torrents/, resume/, settings.json).

🔴 Radarr/Sonarr API — обязательно UrlBase + -L (2026-09-22)

curl http://127.0.0.1:7878/api/v3/system/status       →  HTTP 307, тело ПУСТОЕ
curl http://127.0.0.1:8989/api/v3/system/status       →  HTTP 307, тело ПУСТОЕ

У обеих установок задан UrlBase: /radarr и /sonarr (config/config.xml → <UrlBase>/radarr</UrlBase>). Контейнеры публикуют 7878->7878 / 8989->8989, но приложение слушает под префиксом.

Рабочий вызов:

B=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker
RKEY=$(sudo grep -oE '<ApiKey>[^<]+' "$B/radarr/config/config.xml" | sed 's/<ApiKey>//')
SKEY=$(sudo grep -oE '<ApiKey>[^<]+' "$B/sonarr/config/config.xml" | sed 's/<ApiKey>//')
curl -sL -H "X-Api-Key: $RKEY" "http://127.0.0.1:7878/radarr/api/v3/movie"
curl -sL -H "X-Api-Key: $SKEY" "http://127.0.0.1:8989/sonarr/api/v3/series"

🔴 Симптом ложного вывода: без -L и без префикса curl отдаёт пустое тело и 0 байт в файле дампа — выглядит как «API лежит / ключ неверный». Ключ при этом читается нормально (32 символа). Диагностика: curl -o /dev/null -w "%{http_code}" → 307. Скрипт: /tmp/step1c.sh.

Состояние arr-стека на Кра́кене (2026-09-22)

Версия Записей С файлами
Radarr 6.4.4.10685 34 фильма 12
Sonarr 4.0.20.3014 13 сериалов 4

🔴 У Sonarr 9 из 13 сериалов — фантомы (записи без файлов): Magical Police Department, The Meeting Place Cannot be Changed, MOZU, Delicious in Dungeon, Double or Quits, Severance, Futurama, True Detective, Brassic. Файлы есть только у Our Flag Means Death, Stranger Things, The Cyberfarm, Rick and Morty.

⚠️ Brassic — в Sonarr запись есть, файлы лежат в /media/series/Brassic (Season 1) вне библиотеки Sonarr (/media/series-sonarr/Brassic). Рассинхрон раскладки.

Дамп: /tmp/arr-backup-20260922-215428/ (6 JSON, очереди обеих — 0).

Сверка копий Кра́кен ↔ TrueNAS (2026-09-22)

Отдельный документ: personal/tech/kraken-truenas-media-copies.

Кратко: на TrueNAS (truenas_admin@mallexxx.duckdns.org, пул RED_2TB, /mnt/RED_2TB/storage/) documentaries = 1 файл, documentaries-series = пусто, cartoons-series = 1 сборник. То есть 307 ГБ доксериалов/документалок с Кра́кена копий не имеют. Мультфильмы — копия есть у ~40 из ~104.

⚠️ documentaries не пуста и содержит Храброе сердце.avi (2,18 ГБ) — легко принять за «копии есть», проверив только ls без подсчёта.

⏳ Осталось добить (2026-09-22)

0. 🟡 Удаление медиа на Кра́кене — выполнено 2 из 6 шагов

Инвентаризация: personal/plans/kraken-media-inventory-cleanup. 802 файла / 233 позиции / 1236 ГБ. Свободно 477 ГБ.

Утверждено Alex'ом: 111 позиций, 828,3 ГБ — фильмы полный метр (62 / 485,9 ГБ), сериалы все (5 / 119,7 ГБ), мультфильмы с копией на TrueNAS (14 / 30,1 ГБ), мультфильмы полный метр без копии (30 / 192,6 ГБ). Короткометражки cartoons <0,8 ГБ (72 поз. / 21,7 ГБ) — оставить. Плюс: «C [Transmission] — целиком», недокачанные — тоже сносить.

Порядок: дамп API Radarr/Sonarr → стоп Transmission → чистка раздач A+C → удаление файлов → rescan Radarr/Sonarr/Jellyfin → проверка df/docker → приёмка Alex'ом → доки + коммит.

Шаг Статус
1. Дамп API ✅ /tmp/arr-backup-20260922-215428/
2. Стоп Transmission ✅ Exited (0)
3. Чистка раздач (240) ⏳ план готов, /tmp/tr-remove-ids.txt
4. Удаление файлов ⏳ манифест готов: 41 путь / 693,3 ГБ, ждёт 2 ответа
5–6. Rescan + приёмка ⏳ не начаты

Манифест удаления: /tmp/delete-manifest.txt — 4 папки целиком (movies, movies-radarr, series, series-sonarr) + 37 папок мультфильмов. Все пути проверены -e, отсутствующих 0. Состав movies/movies-radarr сверен с утверждённым списком — 51 + 11 позиций, лишнего нет.

⚠️ Два открытых вопроса блокируют шаг 4:

  1. Добавить 7 пропущенных папок мультфильмов (42,1 ГБ) → 735,4 ГБ вместо 693,3? Пропущены из-за несовпадения имён: смешарики. новый сезон. 2020, Corpse.Bride.2005, Тайна третьей планеты (1981), чебурашка, The.Super.Mario.Bros.Movie.2023, the.secret.life.of.pets.(480p).
  2. documentaries-series (235,6 ГБ) и documentaries (71,9 ГБ) — не в списке, копий на TrueNAS нет. Точно оставляем?

⚠️ В movies/ есть «сироты» — .nfo/.srt/постеры без видео от ранее удалённых фильмов (Master.i.Margarita.2023, Pchelovod.2024, Планета обезьян Новое царство_2024, Леон (авторская версия), Самолетом, поездом, машиной (1987), Ворон_2024). Инвентарь (только видеофайлы) их не видел. Удалять вместе с папкой — правильно.

⚠️ Стект-директории (movies-radarr, series-sonarr) удалять только через API, иначе Radarr держит фантом. Решение Alex по факту: удаляем файлами (манифест целыми папками), rescan после — обязателен.

1. 5 фильмов с Кинопоиск-ID — «здесь пусто», без постера

film/12432920/, film/5454868/, film/11888712/, film/11825624/, film/8184798/ («Рассекая волны: экспедиция „Лас Бальсас“», «Хамнет», «Цена вершины», «Астрал. Могильный ритуал 2», «О добре и зле»).

Причина: href в списке — ссылка на Кинопоиск, TMDB не нашёл (преимущественно фильмы 2025), поэтому id = film/NNNNN/ вместо movie/NNNN. Карточки card-film-...json не существует → клик даёт пустоту; постера нет по той же причине.

Решения (нужен выбор Alex): заглушка с названием + кнопка поиска торрента, либо дорисовывать tmdb_id вручную.

2. ~51 название без постера — проблема ЗАПИСИ В СПИСКЕ, не скрипта

Кэш резолвера ~/.lampac_tmdb_cache.json содержит 51 запись со значением null. По названиям видны три класса:

Пример В списке TMDB знает как
Плохие парни 1/2, Никто 1/2 как есть ДВА фильма в одной строке
Солярис Тарковский, Идиот Бортко как есть «Солярис» (1972) / «Идиот» (2003)
Самолетом, поездом, машиной (1987) как есть «Planes, Trains and Automobiles»

Скрипт не может угадать, что 1/2 — два фильма, а «Тарковский» — уточнение режиссёра. Правится только список руками, не генератором.

3. Секции CUB — проверить в UI

Инжект залит (plugin-cub.js, 35566 Б в отдаваемой сборке). Нужен перезапуск Lampa на ТВ и проверка: секции Watchlist появляются поверх родного фида CUB.

🔴🔴 ШЕСТАЯ СЕССИЯ: ИСТИННАЯ ПРИЧИНА «СЕКЦИИ НЕ ПОЯВИЛИСЬ» — Manifest НЕДОСТУПЕН ВНУТРИ ОБЁРТКИ

Первая версия injectWatchlistParts читала Manifest.catalogs['watchlist'] напрямую. Проба через CDP на живом ТВ показала:

out.cubMainSrc = "function (params, oncomplite, onerror) { original(params, function (cub_data) {
                    try { var wl = Manifest.catalogs['watchlist']; if (!..."
out.err = "ReferenceError: Manifest is not defined"
wlInjected: true          ← обёртка СТОЯЛА
вызов main({}) → len: 4, titles: ["Сейчас смотрят","Рекомендуем посмотреть","Последнее добавление","Огонь!"]
                          ← только родные CUB, НИ ОДНОЙ нашей

Корень: Manifest объявлен локально в замыкании плагина (/catalog.js, IIFE). Обёртка вызывается из другого контекста → ReferenceError → catch → молча oncomplite(cub_data) с родными секциями. Ошибка не видна в UI и не пишется в логи.

Фикс: не полагаться на замыкание — сохранять menu в глобальный window.__wl_menu при loadCatalogs(), в обёртке читать оттуда, с фолбэком на прямой запрос /catalog:

// в loadCatalogs, ПОСЛЕ Manifest.catalogs=json;
if(json['watchlist']&&json['watchlist'].menu)window.__wl_menu=json['watchlist'].menu;

function wlEnsureMenu(cb) {
    if (window.__wl_menu && Object.keys(window.__wl_menu).length) return cb(window.__wl_menu);
    network.silent(Utils.account(Manifest.api_host + '/catalog'), function (json) {
        var m = (json && json['watchlist'] && json['watchlist'].menu) || {};
        window.__wl_menu = m; cb(m);
    }, function () { cb(window.__wl_menu || {}); });
}

wlEnsureMenu вызывается внутри обёртки вместо прямого Manifest.catalogs['watchlist'].

⚠️ Manifest.api_host в wlEnsureMenu — тоже замыкание. Фолбэк сработает только если __wl_menu уже заполнен loadCatalogs; в норме он заполнен. Если __wl_menu пуст И Manifest недоступен — вернётся {} и родной фид.

Сборка плагина: build_plugin.py вместо patch_cub_inject.py

🔴 patch_cub_inject.py отменён — он не идемпотентен. Разрезание по первому function startPlugin + поиск end вставляло INJECT дважды и оставляло файл с нарушенным балансом скобок ({=123, }=122 → node --check → SyntaxError).

Новый сборщик — build_plugin.py, детерминированный:

  1. берёт базу plugin-patched.js;
  2. вставляет window.__wl_menu в loadCatalogs;
  3. вырезает ранее вставленный INJECT по маркеру /* ==== WL-INJECT-START ==== */;
  4. разбивает файл на [до startPlugin] + [startPlugin…] + [хвост })();];
  5. собирает заново: before + INJECT + NEW_START + tail.

Инварианты в конце скрипта (падает с [x] ИНВАРИАНТ НАРУШЕН, если нарушено):

  • WL-INJECT-START = 1
  • function patchCubMain() = 1
  • function startPlugin = 1

⚠️ patchCubMain(); встречается 2 раза (в window.appready и в app:ready-листенере) — это норма, в инвариант не входит (первая версия проверки ошибочно требовала 1).

Проверка: node --check plugin-cub.js + баланс скобок {/} = 0. Итог: 13054 Б, сборка на сервере 35566 → 35440 Б (зависит от длины catalogs-JSON).

Перезапуск Lampa на ТВ — правильный luna-send

🔴 luna-send через ssh 'script -q -c "luna-send … -a \"{...}\""' НЕ парсит аргументы — печатает help. Кавычки в кавычках рвутся. Самая надёжная форма — скрипт ФАЙЛОМ на ТВ:

# 1. положить файл на ТВ
scp tv_restart.sh root@192.168.1.70:/tmp/
# 2. выполнить одной командой (без вложенных кавычек)
ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
#!/bin/sh
# /tmp/tv_restart.sh — выполняется НА ТВ
luna-send -n 1 -f luna://com.webos.applicationManager/close '{"id":"com.lampac.tv"}'
sleep 5
luna-send -n 1 -f luna://com.webos.applicationManager/launch '{"id":"com.lampac.tv"}'

⚠️ Вариант с '"'"' (расшивка кавычек) НЕ сработал — luna-send всё равно печатал help-экран («-a send specified appId in message (default is none)»). Скрипт-файл — единственный проверенный путь. Локальная копия: /Users/admin/lampac-tmp/tv_restart.sh.

⚠️ docker restart lampac НЕ перезагружает плагин на ТВ. После заливки plugin.js ТВ продолжает держать старую версию в памяти — cubMainSrc через CDP показывает старый код. Обязателен перезапуск приложения на ТВ.

Диагностика рантайма Lampa через CDP (websocket-client)

⚠️ Модуля ws в Node нет, websockets в Python нет → использовать websocket-client (установлен). Скрипты в /Users/admin/lampac-tmp/: probe_tv.py, probe_globals.py, probe_cache.py, probe_cub_main.py, probe_inject.py, probe_wait.py, probe_calls.py, probe_ui.py (финальная приёмка — читает заголовки секций из DOM).

WS=$(curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl' | head -1)
python3 probe_ui.py "$WS"

🔴 probe_cub_main.py (вызов cub.main) НЕ годится как приёмка — этот метод невызываем извне (см. «СЕДЬМАЯ СЕССИЯ» в разделе механики) и всегда возвращает pending/4 родные секции, даже когда секции реально работают в UI. Приёмка только через probe_ui.py — переход Lampa.Router.call('main') + чтение document.querySelectorAll('.items-line__title').

Полезные поля пробы probe_tv.py: Lampa.Storage.field('source'), Object.keys(Lampa.Api.sources), Lampa.Api.sources.cub.__wl_injected, typeof Lampa.Api.sources.cub.main.

⚠️ Manifest / Utils / network внутри Runtime.evaluate не видны (ReferenceError) — они в замыкании плагина. Не использовать в пробах. ⚠️ eval(pluginCode) внутри IIFE не создаёт глобальных функций — wlHost(), wlAbs() снаружи дают ReferenceError. Это нормально: cub.main их видит (одно замыкание), внешние пробы — нет.

Питфоллы проверки на ТВ (сводка)

  1. Lampa кэширует манифест и ответы. После правок — полностью выгрузить приложение, не сворачивать. loader.js использует cache_version (9e5 мс ≈ 15 мин), плюс свой localStorage.
  2. luna-send пишет в tty, не в stdout → обёртка script -q -c "luna-send ..." /tmp/o.txt, читать файл. (Доки: family/how-to/tv-luna-send.md.)
  3. Реальный запрос клиента ловить tcpdump на сервере — см. «ПРАВИЛО ДИАГНОСТИКИ» выше.