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

3414 lines
183 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Lampac — развёртывание и эксплуатация (Кра́кен)
created: '2026-09-22'
updated: '2026-09-22'
type: tech
namespace: personal
tags:
- lampac
- torrserver
- docker
- kraken
- streaming
- webos
related:
- '[[personal/plans/lampac-kraken]]'
- '[[personal/plans/lampac-watchlist-plugin]]'
- '[[personal/plans/watchlist-tmdb-missing-marker]]'
- '[[personal/tech/multistream-single-flow-limit]]'
- '[[family/how-to/kraken-network]]'
- '[[family/how-to/tv-luna-send]]'
---
# 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, но это РАЗНЫЕ разделы/диски
```
Правильная проверка маунтов:
```bash
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`:**
```bash
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 медленная, изнашивается, теряется при переустановке/апдейте системы.
**Стало:** пути выводятся из расположения самого скрипта:
```python
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`)
```bash
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`-дампа.
```python
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` → **СНЯТЬ** `❓`». Список нерезолвленных —
ИСТИНА, работает в обе стороны.
```python
# 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 позиций».
```json
{ "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()` тянет имя + год
```python
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**:
```python
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 полей не теряются, запись заменяется целиком).
```python
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-сессия забирала группу процессов с собой. Рабочее решение:
```bash
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]], разбор фикса: [[personal/plans/lampac-pidtor-503-fix]].
#### TorrServer жив и качает
```bash
docker exec lampac sh -c 'curl -s http://127.0.0.1:9085/echo' # -> MatriX.135
```
Тест живым магнитом (Big Buck Bunny) — раздача добавилась и пошла:
```json
{"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**.
Рабочий вызов ровно один:
```bash
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`:
```json
"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**.
Полный разбор: [[personal/plans/lampac-pidtor-503-fix]]
⚠️ **Вторичный фильтр — `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` (из образа):
```json
"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. Полное содержимое:
```json
{
"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 «коммит ДО → правка → заливка → проверка»):**
```bash
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-пути
```python
# БЫЛО — генератор на Кра́кене не находил ключей вообще
for p in (Path("/Users/admin/Developer/watchlist-sync/.env"),):
```
Симптом на Кра́кене: `[ERR] нет TMDB_KEY в .env` → **каталог собирался на одних
Кинопоиск-ссылках, без TMDB-ID и постеров.** Никакой резолв не выполнялся, хотя
код для него был. Именно это объясняет, почему в JSON не было TMDB-ID у половины
позиций — не «слабый резолв», а его полное отсутствие.
**СТАЛО** — перебор 5 мест, печать прочитанных файлов в stderr:
```python
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`
```bash
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`.
**Проверка после заливки — три уровня, все обязательны:**
```bash
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`.
### Боевой прогон (эталон для сравнения)
```bash
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()`:
```python
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` и маркер ❓
Генератор пишет снимок нерезолвленных рядом с каталогом:
```json
{"generated": true, "count": 30, "missing": ["All systems red", "..."]}
```
Маркер ставится **по факту промаха**, а не по тексту `.md`:
```bash
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` → **снять** `❓`, если он есть. Список нерезолвленных —
ИСТИНА, работает в обе стороны.
```python
# 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`
```json
{ "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]] — тот же класс проблемы).
Рабочий шаблон:
```bash
# 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)
```yaml
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`:
```json
{
"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`:
```csharp
string url = $"{init.host}/{(page==1 && init.list?.firstpage != null
? init.list?.firstpage : init.list?.uri)}"; // строка 50
else if (!string.IsNullOrEmpty(cat)) {
var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat));
string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-");
if (!string.IsNullOrEmpty(eval)) { url = CSharpEval(...); } // строки 78–85
if (!url.StartsWith("http")) url = $"{init.host}/{url}"; // строка 87
}
```
**Категория, стоящая ПЕРВОЙ в `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` отбрасывает сайт:
```csharp
if (init == null || !init.enable || init.menu == null || init.hide) continue;
```
**В логах — тишина.** Сайт молча исчезает из `/catalog`. Обе формы (список и словарь) ломают.
`main` Catalog **генерирует сам** из `menu` — в JSON-ответе `/catalog` присутствуют оба
ключа с одинаковым содержимым. Писать `main` в YAML не нужно и нельзя.
⚠️ `menu` **обязателен** — он регистрирует канал, а не подменяет разделы.
### 🔴 Питфолл №4: элемент без `href` выбрасывается
```csharp
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`:
```csharp
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`:
```js
function main$1() {
var parts_limit = 6;
var parts_data = [
function (call) { get$7('?sort=now_playing', params, function (json) {
json.title = Lang.translate('title_now_watch'); call(json); }, call, {life: day*2}); },
function (call) { if (permit.child_small) return call(); ... }
];
...
}
```
| Симптом | Замер CDP | Корень |
|---|---|---|
| `cub.main({})` → вечный `pending`, 0 fetch | `{"pending":true}` через 15 с | `permit` = **undefined** вне замыкания → падение на первой проверке → callback не зовётся |
| секции не добавлялись, ошибок нет | `{"cubMainSrc":"...Manifest.catalogs['watchlist']..."}` | `Manifest` / `Utils` / `network` = **undefined** вне замыкания → `ReferenceError` → `catch` → родной фид |
| «Нет подключения к сети» в логе ТВ | `{localhost}/catalog?uid=...&token={token}` | `Utils.account()` без `Manifest.api_host` подставляет `{localhost}` |
🔴 **Вывод: `cub.main` невызываем извне by design.** Обёртка — тупик, не «недоработка».
#### ✅ Правильный механизм: `Lampa.Api.partNext`
```js
// глобальная функция Lampa; CUB передаёт сюда parts_data — массив функций-секций
function partNext(parts, parts_limit, partLoaded, partEmpty) {
var pieces = parts.filter(function (p) { return typeof p == 'function'; })
.slice(0, 0 + parts_limit);
if (pieces.length) {
new Progress().append(pieces).start(function (result) {
var data = result.filter(function (r) { return r && r.results && r.results.length; });
...
});
} else partEmpty();
}
```
Патч добавляет свои функции-секции в `parts` **до** вызова оригинала:
```js
Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
var menu = window.__wl_menu;
if (menu && Object.keys(menu).length) {
parts.__wl_done = true;
Object.keys(menu).forEach(function (name) {
parts.push(wlSection(name, menu[name]));
});
}
}
return orig.apply(this, arguments);
};
```
Секция = функция `call(json)`, теми же контрактами, что родные CUB:
```js
function wlSection(name, url) {
return function (call) {
wlGet(url, function (json) {
json.title = name;
json.source = 'watchlist'; // ← обязательно, иначе карточка не найдётся
if (Lampa.Arrays.isArray(json.results))
json.results.forEach(function (it) { it.source = 'watchlist'; });
call(json);
}, function () { call(); }); // ошибка → пустой вызов (не ломает пул)
};
}
```
**Преимущество:** работает на **любой** странице, независимо от источника, без `permit`,
без `params`. Родной CUB не подменяется — секции добавляются в общий пул.
#### 🔴 Только ГЛОБАЛЬНЫЕ объекты — внутри замыкания `Manifest`/`Utils`/`network` нет
| Нужно | ❌ Не работает в этом контексте | ✅ Работает |
|---|---|---|
| api_host | `Manifest.api_host` | `Lampa.Manifest.api_host` (фолбэк — хардкод `http://192.168.1.14:9118`) |
| URL-склейка | `Utils.account(url)` → `{localhost}` | **ручная:** `wlAbs(url)` = host + path |
| HTTP | `network.silent(...)` | `new Lampa.Reguest().silent(absURL, ...)`, фолбэк `fetch` |
| список секций | `Manifest.catalogs['watchlist'].menu` | `window.__wl_menu` (заполняется в `loadCatalogs` из `/catalog`) |
```js
function wlAbs(url) {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
return wlHost() + (url.charAt(0) === '/' ? url : '/' + url);
}
```
#### Сборщик `build_plugin.py`
```bash
cd /Users/admin/lampac-tmp
python3 build_plugin.py # plugin-patched.js + INJECT → plugin-cub.js (14376 Б)
node --check plugin-cub.js
```
Детерминирован: режет `[до startPlugin] + INJECT + [новый startPlugin] + '})();'`,
проверяет инварианты (`INJECT=1`, `def=1`, `startPlugin=1`, `network.silent в INJECT=0`).
🔴 **Питфолл прошлого сборщика `patch_cub_inject.py`:** проверки `"X" not in s` шли
**по уже модифицированной строке** → повторный запуск вставлял INJECT дважды → плюс лишняя
`}` от вырезания → `SyntaxError: Unexpected token '}'` при балансе скобок `{: 123, }: 122`.
**Всегда проверять `node --check` + баланс скобок ПЕРЕД заливкой.**
#### 🔴 CDP-пробы на ТВ (порт 9998) — основной инструмент отладки
```bash
WS=$(curl -s "http://192.168.1.70:9998/json/list" \
| jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl')
python3 probe_ui.py "$WS"
```
Питон-клиент: `websocket-client` (`import websocket`). Модуль `websockets` **не установлен**.
`Runtime.evaluate` с `returnByValue: True, awaitPromise: True`.
| Скрипт | Назначение |
|---|---|
| `probe_ui.py` | **приёмка:** `Lampa.Router.call('main')` → чтение заголовков из DOM |
| `probe_inject.py` | инжект `plugin-cub.js` в рантайм **без деплоя** (быстрая итерация) |
| `probe_globals.py` | видимость `Manifest`/`Utils`/`network` |
| `probe_cub_main.py` | сколько секций отдаёт `cub.main` |
| `probe_wait.py` | ждёт результат + перехват `console.log` |
| `probe_tv.py`, `probe_cache.py`, `probe_calls.py` | состояние, кэш, перехват fetch |
🔴 **`eval()` внутри IIFE не создаёт глобальных функций.** После `eval(pluginCode)`
`wlHost()` снаружи → `ReferenceError: wlHost is not defined`. Это **нормально** —
`cub.main` их видит (одно замыкание), внешние пробы нет. Проверять только через
`Lampa.Api.main()` либо чтение DOM.
#### 🔴 Перезапуск Lampa на ТВ: `luna-send` требует файла, не inline-JSON
```bash
# ❌ печатает help: аргументы JSON не проходят через ssh + script -q -c
ssh root@192.168.1.70 'script -q -c "luna-send ... -a {\"id\":\"com.lampac.tv\"}" /dev/null'
# ✅ положить файл на ТВ и выполнить
scp tv_restart.sh root@192.168.1.70:/tmp/
ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
```
```sh
#!/bin/sh
luna-send -n 1 -f luna://com.webos.applicationManager/close '{"id":"com.lampac.tv"}'
sleep 5
luna-send -n 1 -f luna://com.webos.applicationManager/launch '{"id":"com.lampac.tv"}'
```
🔴 **`docker restart lampac` НЕ перезагружает плагин в памяти ТВ** (`catalog.js` кэширован
браузером webOS). Признак: `String(Lampa.Api.sources.cub.main)` содержит старый код.
Полный порядок деплоя: `scp plugin.js` → `chmod 644`/`chown 1000:1000` → `rm ._*` →
`docker restart lampac` (сброс `Staticache`) → **`sh /tmp/tv_restart.sh`** → `probe_ui.py`.
### 🔴 Питфолл №6: карточка без `card_parse` даёт HTTP 500
Без блока `card_parse` Catalog строит URL карточки как `$"{init.host}/{uri}"`
(`CardController.cs:33`) → `http://127.0.0.1:9118/movie/424781` — такого пути нет,
`html == null` → `BadRequest("html")` → **HTTP 500**, в UI «здесь пусто».
**Сигнатура роута (критично!):**
```csharp
[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):**
```yaml
card_parse:
initUrl: |
return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
node: card # фиксированный путь в файле
name: { attribute: name }
original_name: { attribute: original }
year: { attribute: year }
image: { attribute: poster }
description: { attribute: overview }
```
Файлы `wwwroot/cards/card-<media>-<id>.json` (по одному на фильм) с обёрткой
`{"card": {...}}`. По одному файлу — потому что `node` статичен и один общий файл
по `uri` не отфильтровать.
**Диагностика (ошибки карточки в логи Lampac НЕ пишутся!):**
```bash
curl -s -o /tmp/c.json -w "HTTP %{http_code}\n" \
"http://192.168.1.14:9118/catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie"
# тела-маркеры: "html" (URL не найден) | "parse.node" (node не нашёл узел) | "json" (не JSON)
```
### 🔴 Питфолл №5: кэш Catalog
Ключ: `catalog:{plugin}:{search}:{sort}:{cat}:{page}` — **не зависит от содержимого файлов**.
`hybridCache` живёт **в памяти процесса** (не в `cache/fdb/`, не сбрасывается рестартом
контейнера — проверено). После перегенерации JSON выдача может не меняться, пока
ключ тот же. Приём `?nocache={Guid.NewGuid()}` в `format` **не помогает**.
Практический вывод: при отладке менять сам **URL/ключ** (другой `cat` или `sort`),
а не надеяться на инвалидацию кэша.
### `plugin.js` — клиентская часть
Лежит **в образе** (`/lampac/module/Catalog/plugin.js`), не на HDD. Что делает:
- `main()` — итерирует `catalog.main`, собирает секции **СВЕРХУ** родного каталога.
🔴 **НО:** работает это только при выбранном нашем источнике, потому что `Api.getCatalog()`
читает манифест **текущего** источника. Для секций поверх CUB нужен перехват
`Lampa.Api.partNext` — см. «СЕДЬМАЯ СЕССИЯ» выше.
- `menu()` — итерирует `catalog.menu` из YAML
- `full()`/`list()`/`search()` — карточка, категория, поиск
- `Lampa.Listener.follow('menu', ...)` + `e.abort()` — **перехват кликов** по родным
разделам (`movie`/`tv`/`cartoon`/`anime`/`relise`). Срабатывает **только когда
выбран источник «Мой список»**, поэтому родной CUB не трогается при других источниках
- `loadCatalogs()` — сохраняет `window.__wl_menu` из `/catalog` (нужно патчу `partNext`)
### 🔴 Питфолл №6: источник не виден в UI (`enumerable: false`)
```js
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.
## Управление
```bash
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 шагов с бэкапами
и самопроверкой. Запуск:
```bash
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 без ключа = сеть есть).
## Проверка работоспособности
```bash
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` **из Кра́кена**:
```bash
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 падает
```bash
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`), и первая же команда отваливается.
**Каждую команду вызывать отдельно:**
```python
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` не виден внутри.
**Проверка:**
```bash
ssh kraken "sudo docker inspect lampac --format '{{json .Mounts}}'" | jq -r '.[].Destination'
```
**Фикс:**
```bash
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, откат после провала)
```yaml
enable: true
jsonPath: true
displayindex: 1
displayname: Мой список
host: http://127.0.0.1:9118
menu: # ← ОБЯЗАТЕЛЕН (регистрирует канал), единственный блок категорий
- categories:
Фильмы: films
Новинки: new
Мультфильмы: cartoons
Аниме: anime
format:
"-": |
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
search:
uri: watchlist.json
list:
uri: watchlist-films.json
firstpage: watchlist-films.json
total_pages: 1
content:
nodes: "$.items[*]"
name: { attribute: name }
original_name: { attribute: original }
year: { attribute: year }
href: { attribute: href }
image: { attribute: image }
args:
- { name_arg: rating, attribute: rating }
```
### 🔴 `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` ломают одинаково** — и список, и словарь:
```yaml
main: # вариант 1 — список: НЕ работает
- categories:
Фильмы: films
```
```yaml
main: # вариант 2 — словарь: НЕ работает
Фильмы: http://...
```
⚠️ **Ошибка не логируется.** В логах Lampac — тишина (`docker logs lampac | grep -i yaml`
пусто). Сайт просто не появляется в `/catalog`. Диагностика только перебором YAML.
**Кто на самом деле подменяет родные разделы — `plugin.js`, а не YAML:**
```js
Lampa.Listener.follow('menu', function(e){
if (e.type=='action' && (e.action=='tv'||'cartoon'||'anime'||'movie'||'relise')) {
var catalog = Api.getCatalog();
if (!catalog) return;
...
Lampa.Router.call('category', {...});
e.abort(); // ← перехват клика по родному разделу
}
});
```
**Перехват срабатывает ТОЛЬКО когда выбран источник «Мой список».** Родной CUB/tmdb
не трогается, пока источник не переключён на наш.
### 🔴 `host` + `uri` СКЛЕИВАЮТСЯ — `uri` обязан быть относительным
`ListController.cs:53`:
```csharp
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()`), **два вхождения**:
```js
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:
```yaml
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` их не содержит.
```bash
# 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 каталогов).
```json
{
"tmdbId": 1292695, "imdbId": "tt31728330",
"title": "They Will Kill You", "year": 2026,
"hasFile": true, "genres": ["Horror"],
"ratings": {"tmdb": {"value": 6.945}},
"images": [{"coverType": "poster",
"remoteUrl": "https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28Ruu6.jpg"}]
}
```
⚠️ **Названия в Radarr латиницей** (`They Will Kill You`), в `.md` — по-русски.
Перевод делает **TMDB Proxy** (`proxy_tmdb = true` в `lampainit.js`), уже включён.
⚠️ **Связки kp ↔ tmdb в данных нет.** В `.md` — 68 ссылок `kinopoisk.ru/film/<id>/`,
в Radarr — `tmdbId`. Прямого соответствия нет ни в одном файле. **Мост — модуль
`watchlist_sync/resolver.py`** (см. ниже).
### `watchlist_sync/resolver.py` — резолвер постеров (уже написан!)
Путь: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/watchlist-sync/watchlist_sync/`
Уже решает задачу постеров/ID для каталога:
- поиск в **TMDB** (`api.themoviedb.org/3`, языки `ru-RU` + `en-US`)
- поиск в **Кинопоиске** (`api.poiskkino.dev/v1.4`)
- возвращает `Candidate`: `tmdb_id`, `kp_id`, `title`, `year`, `score`, `rating`, `genres`
- `is_horror()` — ужасы по TMDB `genre_id=27` (нужно для сортировки 🧟‍♀️)
- кэш `.resolve_cache.json`; различает `ok` / `ambiguous` (score < 0.75) / `not_found`
### 🔴 `watchlist_sync/arr.py` — ИСТОЧНИК АВТОСКАЧКИ (в коде!)
```python
# 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 мин — пусто.
**Правильные эндпоинты (проверено):**
```bash
# отмена загрузки — файлы на диске НЕ трогаются
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, ...остальное}
```
**Откат:**
```bash
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-хендлер:**
```python
class RedirectKeepMethod(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
if code in (307, 308):
new = urllib.request.Request(newurl, data=req.data,
method=req.get_method())
for k, v in req.headers.items():
new.add_header(k, v)
return new
return super().redirect_request(req, fp, code, msg, headers, newurl)
_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 мин после остановки |
```bash
# конфиг 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` прописывает:
```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 сам:
```bash
# ⚠️ на КОРНЕ, БЕЗ префикса /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:
```bash
# 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`:
```jsonc
"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`.
**Порядок установки:**
```bash
# 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, и прочитать временный файл:
```bash
ssh root@TV 'script -q -c "luna-send -n 1 -f luna://service/method {json}" /tmp/o.txt; cat /tmp/o.txt'
```
Готовый враппер: `~/Automation/tv-luna-send.sh` (`TV_HOST` переопределяет таргет).
Доки: [[family/how-to/tv-luna-send]], [[family/how-to/htpc-webos-luna]].
⚠️ **Установка идёт через `com.webos.appInstallService/dev/install`**, а НЕ через
`org.webosbrew.hbchannel.service/install` (последний в образе есть, но `luna-send` его не дёрнул чисто).
⚠️ `luna-send -i` (subscribe, бесконечный) через враппер **не работает** — нужен polling.
### Первый просмотр — проверено 2026-09-22
Фильм запущен, **не дёргается**. Alex: «Вроде стабильно, не делается».
| Факт | Значение |
|---|---|
| Соединения ТВ → Lampac | 3 × ESTABLISHED `192.168.1.70 → 192.168.1.14:9118` |
| Нагрузка контейнера | `lampac` CPU 57%, NET 168 МБ ↓ / 132 МБ ↑ |
| Битрейт на eth0 | **~3–4 Мбит/с** |
⚠️ **Почему всего 3–4 Мбит, а не 15:** выбран источник **«Смотреть онлайн (lampac 1.18…)»** — встроенный
плагин `online.js`, HLS-поток. Lampac отдаёт **ссылку**, ТВ тянет видео напрямую из CDN (в `/proc/net/tcp`
ТВ видны внешние адреса на `:443`/`:8443`). Тяжёлый remux с торрента не участвует — поэтому и не дёргается.
🔴 **Следствие:** этот просмотр **не проверяет**, держит ли сеть remux 60–80 Мбит.
Тест функциональности, не полосы. Полосу проверять на торрент-источнике.
**Как убрать выбор источника:** Lampa → Настройки → Плагины → выключить `online.js`. Останутся только торренты.
## 🔴 Выбор источника каталога в Lampa (проверено 2026-09-22)
**В Lampa НЕТ пункта «Настройки → Источник».** Я его выдумал — Alex дважды указал на ошибку.
Не повторять.
Источник (Rezka / Kinobase / «Мой список» / …) переключается **из меню каталогов на главной**:
1. Открыть Lampa на главной
2. Встать на любой ряд каталога → нажать **влево** на пульте
3. Слева выедет **панель со списком источников**
4. Выбрать нужный → OK; каталог перестроится, в заголовке появится имя источника
в верхнем регистре («Фильмы — МОЙ СПИСОК»)
**Плагин `catalog.js` не добавляет пункт меню — он подменяет существующие.**
Механика: при старте приложения `Lampa.Utils.account(api_host + '/catalog', …)` получает список
каталогов, регистрирует каждый как `Lampa.Params.values.source[key]` и подменяет
`Lampa.Api.sources[key]`. Затем хук `Lampa.Listener.follow('menu', …)` перехватывает клики по
`movie`/`tv`/`cartoon`/`anime`/`relise` и подставляет URL из `catalogs[key].menu`.
⚠️ **Следствие — каталоги регистрируются ТОЛЬКО при старте приложения.**
Добавил плагин и не перезапустил Lampa → источника не будет, `/catalog` не запрашивается,
соединений с `:9118` ноль. Нужен **полный перезапуск** (не сворачивание).
### ⚠️ Клиент на ТВ — это не «отдельная Lampa»
`com.lampac.tv` (webOS) грузит UI **с сервера**: `loader.js` читает `lampac_url` из localStorage
(дефолт `http://192.168.1.14:9118`) и подключает `app.min.js` + `lampainit.js`.
Значит логика каталога на ТВ = **серверная**, и править её надо на Lampac, а не на ТВ.
`/lampainit.js` содержит `var plugins_add = [...]` со строкой
`{"url":".../catalog.js","name":"Альтернативные источники каталога","status":1}` —
это **серверный список плагинов, которые Lampac сам подсовывает** в UI.
Каталог watchlist работает через **модуль `Catalog`**, не через эту строку.
Не искать причину поломки в `plugins_add`.
### 🔴 БАГ Lampac: источник не виден в панели (`enumerable: false`)
**Найдено 2026-09-22** диагностикой через Chrome DevTools ТВ. Симптом Alex:
«Ни на главной ни в Фильмах нет! Там только Сейчас смотрят, рекомендуем итд».
**Факты с живого ТВ:**
| Замер | Значение |
|---|---|
| `Lampa.Storage.field('source')` | `'Мой список'` — источник **уже выбран** |
| `Object.keys(Lampa.Api.sources)` | `'tmdb|cub'` — ❌ источника нет в перечислении |
| `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()`, **два вхождения**):
```js
// БЫЛО (баг)
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:
```yaml
# docker-compose.yml
- ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro
```
```bash
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 этого не дают.
```bash
# цель отладки + 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`:
```python
pages = json.load(urllib.request.urlopen("http://192.168.1.70:9998/json", timeout=8))
target = next(p for p in pages if "com.lampac.tv" in p.get("url",""))
ws = websocket.create_connection(target["webSocketDebuggerUrl"], timeout=10)
# Runtime.evaluate: Lampa.Storage.field('source')
# Object.keys(Lampa.Api.sources)
# 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 <br>ноль = Lampa не стартовала |
| Источник в списке | DevTools: `Object.keys(Lampa.Api.sources)` | **все** каталоги, вкл. «Мой список» |
Порядок разбора: сначала сеть с ТВ → потом `netstat` на ТВ → потом `/catalog` на сервере →
потом DevTools (состояние JS). **Если источник есть в `Api.sources` напрямую, но нет в
`Object.keys()` — это баг `enumerable` в `plugin.js`, а не проблема настроек.**
⚠️ `docker logs lampac` **HTTP-запросы не пишет** — по логам факт обращения ТВ не проверить.
## Внутреннее устройство Lampac (разведка 2026-09-22)
**33 модуля** в образе (`/lampac/module/`):
`AdminPanel`, `Adult`, **`Catalog`**, `Community`, **`DLNA`**, `DatabaseEditor`, `ExternalBind`,
`ForkPlayerXML`, `GStreamer`, `JacRed`, `Kit`, `LampaWeb`, `LampacApk`, `LogUserRequest-Lite`,
`MsxNative`, `Music`, `NextHUB`, **`Online`**, `OnlineAnime`, `OnlineENG`, `OnlineGEO`, `OnlinePaid`,
`OnlineRUS`, `OnlineUKR`, `PidTor`, `Potok`, `Proxy`, `SISI`, `Sync`, `Telemetry` (+ остальные).
**Конфиг — `current.conf`** внутри образа (`/lampac/current.conf`, 157 КБ, генерируется из `base.conf` +
`init.conf`). Рабочий каталог `/lampac`, файлы читаются **из корня**, не из `config/`.
Выключение модуля — двумя механизмами:
1. **`SkipModules`** в `init.conf` / `current.conf`
2. **`manifest.json`** в каталоге модуля (`"enable": true|false`)
⚠️ **`init.conf` НЕ в образе по умолчанию** — его читают из `/lampac/init.conf`, файл перекрывает
`base.conf`. Правка через `docker exec` **теряется при пересоздании контейнера**, поэтому
смонтирован с HDD (`./config-module/init.conf:/lampac/init.conf:ro`). См. раздел docker-compose выше.
⚠️ `current.conf` в образе (157 КБ) — **сгенерированный** результат (`guid`, `listen`, полный конфиг).
Это **не** входной файл, править его бессмысленно.
### Полезные роуты (проверены curl'ом)
| Роут | Код | Что |
|---|---|---|
| `/` | 200 | LampaWeb (UI, `<base href="/lampa-main/">`) |
| `/online.js` | 200 | плагин VOD — 70+ онлайн-источников |
| `/lg.ipk` | 200 | установщик клиента для LG (6.9 МБ) |
| `/sisi` | 200 | JSON-список платформ 18+ |
| `/dlna` | 200 | DLNA-модуль |
| `/ts/echo` | 200 | TorrServer (отдаёт версию, напр. `MatriX.135`) |
| `/catalog.js` | **200** | плагин каталогов (включён 2026-09-22) — см. раздел Catalog |
| `/catalog` | **200** | JSON каналов каталогов |
| `/watchlist.json`, `/watchlist-<code>.json` | **200** | наш JSON-каталог watchlist |
| `/cub`, `/api`, `/lampac.js` | 404/500 | — |
⚠️ `wwwroot` отдаётся статикой: **любой** файл, положенный туда, доступен по HTTP
от корня (`http://<host>:9118/<файл>`). Проверено записью пробного файла.
## Radarr/Sonarr как источник метаданных для каталога (2026-09-22)
**Зачем:** каталог watchlist нуждается в **TMDB ID и постерах** для элементов.
Список `movies-watchlist.md` — просто текст, там этого нет. Но фильмы добавлялись
в **Radarr**, который уже держит метаданные (тянет из TMDB при добавлении).
### Что отдаёт Radarr API
```bash
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 .
```
```json
{
"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
```bash
curl -s "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # ❌ 0 байт
curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # ✅ 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`:
```bash
ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq .BitTorr
```
```json
{ "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**.
```bash
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`:**
```bash
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:
```bash
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 — замер
```bash
ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.json
# download-dir /downloads · rpc-port 9091 · speed-limit-down-enabled false
# /config/torrents/*.torrent = 257 файлов (накопленный список РАЗДАЧ, не активная качка)
# CPU 0.76% — простаивает. NetIO 290G↑/26.7G↓ (суммарно за жизнь контейнера)
```
Полный состав контейнеров Кра́кена:
`lampac · radarr · jellyfin · transmission · portainer · flaresolverr ·
prowlarr · sonarr · watchtower · rclone · xray-reverse-bridge · hermes-kraken · cloudflared`
## Что осталось
1. **Лимит кэша TorrServer** — сейчас дефолт (`CacheSize` 96 МБ, `UseDisk: false`).
Диск 396 ГБ свободно, на HDD TorrServer не пишет вовсе — вопрос неактуален.
2. **Шаг 7 плана — watchlist в UI Lampa** — ✅ **серверная часть сделана** 2026-09-22,
🔴 **архитектура признана неверной**, переделывается: см.
[[personal/plans/lampac-watchlist-plugin]], разделы «ПЕРЕДЕЛКА 2» и
«✅ ПРОВЕРКА "как это реализуемо"».
3. **Проверка полосы на торрент-источнике** (remux 60–80 Мбит) — не сделана.
4. **Постеры/карточка в каталоге** — не сделаны, `image` пустой. Решение — брать
из Radarr (`images[].remoteUrl`) или `MediaCover/`, см. раздел выше.
5. **Автоскачка Radarr + Sonarr** — 45 monitored / очередь 6 у Radarr,
13 monitored / очередь 19 у Sonarr. План отключения на апруве
([[personal/plans/lampac-watchlist-plugin]], «ПЕРЕДЕЛКА 2»).
## Карточка фильма в модуле Catalog — рабочая схема (проверено 2026-09-22)
Модуль `Catalog` (в образе `/lampac/module/Catalog/`). Каталог watchlist:
`sites/watchlist.yaml` + JSON в `wwwroot/`.
### Списки
```yaml
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)`),
подстановок нет; `/` = вложенность.
Рабочая конфигурация:
```yaml
card_parse:
initUrl: |
return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
node: card
name: { attribute: name }
original_name: { attribute: original }
year: { attribute: year }
image: { attribute: poster }
description: { attribute: overview }
```
Файл `wwwroot/cards/card-movie-424781.json`:
```json
{ "card": { "name": "...", "title": "...", "original": "...", "original_title": "...",
"year": "...", "rating": "...", "poster": "...", "img": "...",
"overview": "...", "genres": "...", "cast": "...", "runtime": 116 } }
```
Ключи дублируются (`name`+`title`, `poster`+`img`): `CardController` отдаёт
`title`/`original_title`, а `card_parse` ищет `name`/`original`.
### Эндпоинты
| Запрос | Назначение |
|---|---|
| `GET /catalog` | список каналов; `main` = категории |
| `GET /catalog/list?plugin=watchlist&cat=films&page=1` | элементы секции |
| `GET /catalog/card?plugin=watchlist&uri=movie/424781` | **карточка** |
🔴 **Не по `/card`** — там 404 и ложный вывод «карточка сломана».
### Файлы и права
- Статика раздаётся из `/lampac/wwwroot/` (`core` UseStaticFiles).
- Владделец — `1000:1000` (пользователь `lampac` в контейнере), каталоги `755`,
файлы `644`. Иначе `Permission denied` → **404**.
- macOS `tar` → AppleDouble `._*`, добавлять `--no-mac-metadata`.
### Деплой
```bash
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` при клике:
```js
if(json.tmdb_id){ status.need++; Lampa.Api.sources.cub.reactionsGet(
{id: json.tmdb_id, method: json.original_name ? 'tv' : 'movie'}, ...); }
```
Без `tmdb_id` (**числа**, не строки!) Lampa считает карточку неполной → пустой экран.
### Два бага в цепочке, оба надо было починить
1. **`ModInit.cs:285` — `nodeValue` возвращает ЛЮБОЕ значение как строку.**
```csharp
string value = valueToken switch {
JValue jValue => jValue.Value?.ToString(), // ← число → "424781"
... };
```
Поэтому `tmdb_id` из JSON приходил строкой, и `if(json.tmdb_id)` давала не число.
2. **Кэш в памяти процесса переживает `docker restart`.**
`hybridCache` ключ `catalog:card:{plugin}:{uri}` живёт в памяти. При правке
данных старые ответы продолжают отдаваться. **Обязательно `docker restart lampac`
и проверять на НЕзакэшированном uri.**
### Фикс — `format` в `card_args`
`format` исполняется как C# (`CSharpEval`, объект `CatalogNodeValue(value, host)`)
и может вернуть настоящий тип, а не строку:
```yaml
card_args:
- name_arg: tmdb_id
attribute: tmdb_id
format: "long.TryParse(value, out long v) ? v : 0"
- name_arg: source
attribute: media_type
- name_arg: 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 показывает в бейдже
```
### Рабочий ответ карточки (эталон)
```json
{ "id": "movie/424781", "title": "Простите за беспокойство",
"original_title": "Простите за беспокойство", "tmdb_id": 424781,
"source": "movie", "runtime": 111, "rating": "6.8", "vote_average": 68,
"genres": [{"name":"фэнтези"},{"name":"фантастика"},{"name":"комедия"}],
"overview": "...", "img": "http://192.168.1.14:9118/proxyimg:210:0/..." }
```
**Результат: 352 из 352 карточек отдают числовой `tmdb_id`.**
### Про `CUB` — почему «только фильмы из списка»
CUB и «Мой список» — **два независимых источника Lampa**, одновременно показать
нельзя. Переключение: **Настройки → Остальное → Основной источник**.
Пункта «Источник» в настройках НЕ существует.
### Питфолл проверки на ТВ
Lampa на ТВ кэширует манифест и ответы. После правок **надо полностью
выгрузить приложение** (не свернуть), иначе клиент держит старый кэш.
---
## 🔴 Анатомия CUB-источника — что нужно, чтобы карточка открылась (разобрано 2026-09-22)
Разбор `app.min.js` (2.1 МБ; Лампа грузится с сервера, файл живёт в
`/lampac/wwwroot/lampa-main/app.min.js`). Копия локально:
```bash
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 }`
```js
var CUB = { main, menu, full: full$1, list, category, clear, person,
seasons, menuCategory, discovery, reactionsGet, reactionsAdd,
metadataGet, discussGet, extensions };
```
### `CUB.full()` (строка ~34240) — эталон карточки
```js
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`)
```js
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`:
```js
function main$1() {
var parts_limit = 6;
var parts_data = [
function (call) { get$7('?sort=now_playing', params, function (json) {
json.title = Lang.translate('title_now_watch'); call(json); }, call, {life: day*2}); },
function (call) { if (permit.child_small) return call(); ... }
];
...
}
```
| Симптом | Замер CDP на ТВ | Корень |
|---|---|---|
| `cub.main({})` → вечный `pending`, 0 fetch | `{"pending":true}` через 15 с | `permit` = **undefined** вне замыкания → падение на первой проверке → callback не зовётся |
| секции не добавлялись, ошибок нет | `cubMainSrc` = `...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)`:
```js
function partNext(parts, parts_limit, partLoaded, partEmpty) {
var pieces = parts.filter(function (p) { return typeof p == 'function'; })
.slice(0, 0 + parts_limit);
if (pieces.length) {
new Progress().append(pieces).start(function (result) {
var data = result.filter(function (r) { return r && r.results && r.results.length; });
...
});
} else partEmpty();
}
```
Патч добавляет наши функции-секции в `parts` **до** вызова оригинала:
```js
Lampa.Api.partNext = function (parts, parts_limit, partLoaded, partEmpty) {
if (Lampa.Arrays.isArray(parts) && !parts.__wl_done) {
var menu = window.__wl_menu;
if (menu && Object.keys(menu).length) {
parts.__wl_done = true;
Object.keys(menu).forEach(function (name) {
parts.push(wlSection(name, menu[name]));
});
}
}
return orig.apply(this, arguments);
};
```
Секция — функция `call(json)`, те же контракты, что у родных CUB:
```js
function wlSection(name, url) {
return function (call) {
wlGet(url, function (json) {
json.title = name;
json.source = 'watchlist'; // ← обязательно, иначе карточка уйдёт в 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`) |
```js
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`)
```bash
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` + баланс ПЕРЕД заливкой.
#### Деплой (полный порядок)
```bash
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` подменяет разделы через:
```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'ом:
```bash
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`:
```csharp
string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site;
```
Без явного `plugin` ключ = **`displayname`** = «Мой список». ТВ берёт `plugin`
именно оттуда и шлёт его синхронно в `/catalog/list` и `/catalog/card`.
`ModInit.goInit` строки **51-52** — очистка имени перед поиском файла:
```csharp
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
```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` на сервере во время клика:
```bash
ssh kraken "sudo timeout 40 tcpdump -i any -n -A 'host 192.168.1.70 and port 9118' 2>/dev/null | grep -aE 'GET /|HTTP/1'"
```
## Права, tar и macOS-мусор при заливке `cards/`
Три отдельных питфолла, каждый даёт **молчаливый 404** на статике:
1. **`macOS tar` кладёт AppleDouble-файлы `._*`** в архив → мусор в `wwwroot`.
Фикс: `tar --no-mac-metadata` + `sudo rm -f .../._*`.
2. **Права.** Если файлы приехали `root:root 600` — dotnet отдаёт **`Permission denied`**,
статика отвечает **404** (не 403!), и это выглядит как «файла нет».
Фикс: `chown -R 1000:1000` (пользователь `lampac` в контейнере) + `chmod -R 755`,
затем `chmod -R a+rX`.
3. **`find -exec` через `ssh -t` рвётся** — `find: missing argument to '-exec'`
(tty съедает `{} \;`). Фикс: два отдельных `chmod -R` вместо `find -exec`.
⚠️ Также: `&&`-цепочка в `ssh -t` рвёт tty-сессию («Connection closed»).
Каждую команду — отдельным вызовом.
## 🔴 Staticache — `/catalog.js` собирается и КЭШИРУЕТСЯ
`/catalog.js` — **не файл**, а **сборка**: Lampac берёт `plugin.js` из
`/lampac/module/Catalog/plugin.js`, подставляет `api_host` и `catalogs`
(отсюда ~18 КБ разницы от исходника) и кэширует результат через
атрибут `[Staticache]`.
**Питфолл, который стоил времени:** залил новый `plugin.js` (14099 Б,
патч на месте — `grep` подтверждает), а `/catalog.js` продолжал отдавать
**старую сборку** (32205 Б, патча нет). Выглядело как «патч не применяется».
```
=== патч в контейнере:
14099 /lampac/module/Catalog/plugin.js ← патч ЕСТЬ
=== что отдаёт клиенту:
32205 байт, injectWatchlistParts: 0 ← патча НЕТ
```
**Лечение:** `docker restart lampac` (сбрасывает Staticache-кэш), затем
проверять **размер и маркеры в отдаваемом файле**, а не только в файле на диске:
```bash
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`):
```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']`:
```js
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`:
```js
// в loadCatalogs, ПОСЛЕ Manifest.catalogs=json;
if(json['watchlist']&&json['watchlist'].menu)window.__wl_menu=json['watchlist'].menu;
function wlEnsureMenu(cb) {
if (window.__wl_menu && Object.keys(window.__wl_menu).length) return cb(window.__wl_menu);
network.silent(Utils.account(Manifest.api_host + '/catalog'), function (json) {
var m = (json && json['watchlist'] && json['watchlist'].menu) || {};
window.__wl_menu = m; cb(m);
}, function () { cb(window.__wl_menu || {}); });
}
```
`wlEnsureMenu` вызывается внутри обёртки вместо прямого `Manifest.catalogs['watchlist']`.
⚠️ **`Manifest.api_host` в `wlEnsureMenu` — тоже замыкание.** Фолбэк сработает только
если `__wl_menu` уже заполнен `loadCatalogs`; в норме он заполнен. Если `__wl_menu`
пуст И `Manifest` недоступен — вернётся `{}` и родной фид.
### Сборка плагина: `build_plugin.py` вместо `patch_cub_inject.py`
🔴 **`patch_cub_inject.py` отменён — он не идемпотентен.** Разрезание по первому
`function startPlugin` + поиск `end` вставляло `INJECT` **дважды** и оставляло файл
с нарушенным балансом скобок (`{`=123, `}`=122 → `node --check` → `SyntaxError`).
**Новый сборщик — `build_plugin.py`, детерминированный:**
1. берёт базу `plugin-patched.js`;
2. вставляет `window.__wl_menu` в `loadCatalogs`;
3. вырезает ранее вставленный INJECT по маркеру `/* ==== WL-INJECT-START ==== */`;
4. разбивает файл на `[до startPlugin]` + `[startPlugin…]` + `[хвост })();]`;
5. собирает заново: `before` + `INJECT` + `NEW_START` + `tail`.
**Инварианты в конце скрипта** (падает с `[x] ИНВАРИАНТ НАРУШЕН`, если нарушено):
- `WL-INJECT-START` = **1**
- `function patchCubMain()` = **1**
- `function startPlugin` = **1**
⚠️ `patchCubMain();` встречается **2 раза** (в `window.appready` и в `app:ready`-листенере) —
это норма, в инвариант не входит (первая версия проверки ошибочно требовала 1).
Проверка: `node --check plugin-cub.js` + баланс скобок `{`/`}` = 0.
Итог: **13054 Б**, сборка на сервере **35566 → 35440 Б** (зависит от длины `catalogs`-JSON).
### Перезапуск Lampa на ТВ — правильный `luna-send`
🔴 **`luna-send` через `ssh 'script -q -c "luna-send … -a \"{...}\""'` НЕ парсит аргументы** —
печатает help. Кавычки в кавычках рвутся. **Самая надёжная форма — скрипт ФАЙЛОМ на ТВ:**
```bash
# 1. положить файл на ТВ
scp tv_restart.sh root@192.168.1.70:/tmp/
# 2. выполнить одной командой (без вложенных кавычек)
ssh root@192.168.1.70 'sh /tmp/tv_restart.sh'
```
```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).
```bash
WS=$(curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl' | head -1)
python3 probe_ui.py "$WS"
```
🔴 **`probe_cub_main.py` (вызов `cub.main`) НЕ годится как приёмка** — этот метод
невызываем извне (см. «СЕДЬМАЯ СЕССИЯ» в разделе механики) и всегда возвращает
`pending`/4 родные секции, **даже когда секции реально работают в UI**.
**Приёмка только через `probe_ui.py`** — переход `Lampa.Router.call('main')` + чтение
`document.querySelectorAll('.items-line__title')`.
Полезные поля пробы `probe_tv.py`: `Lampa.Storage.field('source')`,
`Object.keys(Lampa.Api.sources)`, `Lampa.Api.sources.cub.__wl_injected`,
`typeof Lampa.Api.sources.cub.main`.
⚠️ `Manifest` / `Utils` / `network` внутри `Runtime.evaluate` **не видны**
(`ReferenceError`) — они в замыкании плагина. Не использовать в пробах.
⚠️ **`eval(pluginCode)` внутри IIFE не создаёт глобальных функций** — `wlHost()`,
`wlAbs()` снаружи дают `ReferenceError`. Это нормально: `cub.main` их видит
(одно замыкание), внешние пробы — нет.
## Питфоллы проверки на ТВ (сводка)
1. **Lampa кэширует манифест и ответы.** После правок — **полностью выгрузить
приложение**, не сворачивать. `loader.js` использует `cache_version`
(9e5 мс ≈ 15 мин), плюс свой `localStorage`.
2. `luna-send` пишет в **tty**, не в stdout → обёртка
`script -q -c "luna-send ..." /tmp/o.txt`, читать файл.
(Доки: `family/how-to/tv-luna-send.md`.)
3. Реальный запрос клиента ловить `tcpdump` на сервере — см. «ПРАВИЛО ДИАГНОСТИКИ» выше.