183 KiB
title, created, updated, type, namespace, tags, related
| title | created | updated | type | namespace | tags | related | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Lampac — развёртывание и эксплуатация (Кра́кен) | 2026-09-22 | 2026-09-22 | tech | personal |
|
Lampac — развёртывание и эксплуатация (Кра́кен)
Технический справочник. Развёрнуто 2026-09-22. План и статус шагов — personal/plans/lampac-kraken.
Что это
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+ файлов).
Прод-генератор жил там без версионирования — ошибка, исправлено.
Репозиторий: /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)
└── 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 /Users/admin/lampac-tmp
python3 build_plugin.py # plugin-patched.js + INJECT → plugin-cub.js (14376 Б)
node --check plugin-cub.js
Детерминирован: режет [до startPlugin] + INJECT + [новый startPlugin] + '})();',
проверяет инварианты (INJECT=1, def=1, startPlugin=1, network.silent в INJECT=0).
🔴 Питфолл прошлого сборщика patch_cub_inject.py: проверки "X" not in s шли
по уже модифицированной строке → повторный запуск вставлял INJECT дважды → плюс лишняя
} от вырезания → SyntaxError: Unexpected token '}' при балансе скобок {: 123, }: 122.
Всегда проверять node --check + баланс скобок ПЕРЕД заливкой.
🔴 CDP-пробы на ТВ (порт 9998) — основной инструмент отладки
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из YAMLfull()/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)
Локальный скрипт /Users/admin/lampac-tmp/deploy_lampac.py — 7 шагов с бэкапами
и самопроверкой. Запуск:
python3 /Users/admin/lampac-tmp/deploy_lampac.py # полный цикл
python3 /Users/admin/lampac-tmp/deploy_lampac.py --skip-plugin # только JSON+YAML
Порядок: бэкап JSON в wwwroot/.bak/ → заливка 5 JSON → watchlist.yaml →
патченный plugin.js → volume в compose (если нет) → docker compose up -d →
проверка (/catalog, 4 категории с подсчётом постеров и TMDB-ID, факт патча).
Генератор каталога (gen_watchlist_v2.py)
/Users/admin/lampac-tmp/gen_watchlist_v2.py <watchlist.md> <out_dir>
Читает .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()— ужасы по TMDBgenre_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 / «Мой список» / …) переключается из меню каталогов на главной:
- Открыть Lampa на главной
- Встать на любой ряд каталога → нажать влево на пульте
- Слева выедет панель со списком источников
- Выбрать нужный → 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/.
Выключение модуля — двумя механизмами:
SkipModulesвinit.conf/current.confmanifest.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
Что осталось
- Лимит кэша TorrServer — сейчас дефолт (
CacheSize96 МБ,UseDisk: false). Диск 396 ГБ свободно, на HDD TorrServer не пишет вовсе — вопрос неактуален. - Шаг 7 плана — watchlist в UI Lampa — ✅ серверная часть сделана 2026-09-22, 🔴 архитектура признана неверной, переделывается: см. personal/plans/lampac-watchlist-plugin, разделы «ПЕРЕДЕЛКА 2» и «✅ ПРОВЕРКА "как это реализуемо"».
- Проверка полосы на торрент-источнике (remux 60–80 Мбит) — не сделана.
- Постеры/карточка в каталоге — не сделаны,
imageпустой. Решение — брать из Radarr (images[].remoteUrl) илиMediaCover/, см. раздел выше. - Автоскачка 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/(coreUseStaticFiles). - Владделец —
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 считает карточку неполной → пустой экран.
Два бага в цепочке, оба надо было починить
-
ModInit.cs:285—nodeValueвозвращает ЛЮБОЕ значение как строку.string value = valueToken switch { JValue jValue => jValue.Value?.ToString(), // ← число → "424781" ... };Поэтому
tmdb_idиз JSON приходил строкой, иif(json.tmdb_id)давала не число. -
Кэш в памяти процесса переживает
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 на статике:
macOS tarкладёт AppleDouble-файлы._*в архив → мусор вwwwroot. Фикс:tar --no-mac-metadata+sudo rm -f .../._*.- Права. Если файлы приехали
root:root 600— dotnet отдаётPermission denied, статика отвечает 404 (не 403!), и это выглядит как «файла нет». Фикс:chown -R 1000:1000(пользовательlampacв контейнере) +chmod -R 755, затемchmod -R a+rX. 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)
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, детерминированный:
- берёт базу
plugin-patched.js; - вставляет
window.__wl_menuвloadCatalogs; - вырезает ранее вставленный INJECT по маркеру
/* ==== WL-INJECT-START ==== */; - разбивает файл на
[до startPlugin]+[startPlugin…]+[хвост })();]; - собирает заново:
before+INJECT+NEW_START+tail.
Инварианты в конце скрипта (падает с [x] ИНВАРИАНТ НАРУШЕН, если нарушено):
WL-INJECT-START= 1function patchCubMain()= 1function 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 их видит
(одно замыкание), внешние пробы — нет.
Питфоллы проверки на ТВ (сводка)
- Lampa кэширует манифест и ответы. После правок — полностью выгрузить
приложение, не сворачивать.
loader.jsиспользуетcache_version(9e5 мс ≈ 15 мин), плюс свойlocalStorage. luna-sendпишет в tty, не в stdout → обёрткаscript -q -c "luna-send ..." /tmp/o.txt, читать файл. (Доки:family/how-to/tv-luna-send.md.)- Реальный запрос клиента ловить
tcpdumpна сервере — см. «ПРАВИЛО ДИАГНОСТИКИ» выше.