[2026-09-22] eagle: personal/plans/lampac-watchlist-plugin.md personal/tech/lampac-deployment.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-22 13:29:57 +06:00
1 parent bd00f6f145
commit 5e389f93d7
2 files changed
+232 -1

No files matched your search

+183 -1
View File
@@ -7,7 +7,7 @@ related:
- '[[family/projects/watchlist-sync]]'
- '[[family/projects/watchlist-automation]]'
- '[[family/documents/movies-watchlist]]'
status: rework
status: card-pending
tags:
- lampac
- lampa
@@ -20,6 +20,7 @@ tags:
- sonarr
- torrserver
- tmdb
- card-parse
title: Watchlist в UI Lampa — каталог из списка
type: plan
updated: '2026-09-22'
@@ -94,6 +95,131 @@ updated: '2026-09-22'
> `deploy_lampac.py` (заливка + проверка одной командой), обновлённый `docker-compose.yml`
> (volume на `plugin.js`). Правка `arr.py` применена и **закоммичена `9872016`**.
> См. **«🟢 РЕШЕНИЕ: первая категория игнорирует format»** — читать ПЕРВЫМ.
>
> **Обновление 2026-09-22 (ночь, третья сессия): ✅ ДЕПЛОЙ ПРОШЁЛ, ПОСТЕРЫ И ПОИСК В UI ЕСТЬ. КАРТОЧКА В РАБОТЕ.**
> Всё залито на Кра́кен и **проверено на живом сервере**:
> - «Мой список» первый в `/catalog`, `main` = `[Новинки, Фильмы, Мультфильмы, Аниме]`;
> - **films 360 / постеров 308**, new 33/30, cartoons 3/3, anime 7/7;
> - `id` элементов = **`movie/424781`** — рабочие TMDB-карточки (не поиск Кинопоиска);
> - патч `plugin.js` **применён** (`enumerable:true`, файл 10738 Б), volume смонтирован —
> проверено `docker inspect`, переживёт обновление образа;
> - `arr.py` фикс в бою, Radarr/Sonarr monitored 0, RSS 0, очередь 0.
>
> 🔴 **ОСТАЛОСЬ ОДНО: карточка по клику.** Была HTTP 500 → сейчас **HTTP 400 `parse.node`**.
> Корень найден по коду: `CardController.Index(plugin, uri, type)` — **параметра `cat` НЕТ**
> (поэтому `{cat}` в `initUrl` пустой → `cards-.json` → 404 → 500), а **`parse.node` —
> статичный JSONPath** (`json.SelectToken(parse.node)`, строка 95), подстановок не делает,
> и `/` в пути = вложенность.
>
> **Решение дописано, но НЕ залито:** файл на каждый фильм `wwwroot/cards/card-movie-NNN.json`
> с обёрткой `{"card": {...}}`, `initUrl` собирает путь C#-выражением
> `$"{host}/cards/card-{uri.Replace("/", "-")}.json"`, `node: card`.
> `deploy_lampac.py` пока НЕ умеет лить каталог `cards/` — доклеить `scp -r`.
> См. **«🔴 КАРТОЧКА: механика card_parse»**.
## 🔴 КАРТОЧКА: механика card_parse (разобрано по коду 2026-09-22)
Клик по элементу каталога ведёт в `/catalog/card?plugin=watchlist&uri=<id>&type=movie`.
Без `card_parse` Catalog строит URL как `$"{init.host}/{uri}"` → `http://127.0.0.1:9118/movie/424781`
— такого пути нет, **HTTP 500**, в UI «здесь пусто».
**Что даёт `card_parse` (по `CardController.cs`):**
| Поле | Механика | Подстановки |
|---|---|---|
| `initUrl` | исполняется **C#-выражением** через `CSharpEval`, объект `CatalogInitUrlCard(host, args, uri, query, type)` | ✅ **есть `uri`, `type`, `host`** |
| `node` | `json.SelectToken(parse.node)` — **чистый JSONPath** | ❌ **нет**, статичен |
| `name` / `image` / `description` | `ModInit.nodeValue(...)` → `attribute` | — |
**Сигнатура роута (критично):**
```csharp
[Route("catalog/card")]
public async Task<ActionResult> Index(string plugin, string uri, string type)
```
🔴 **Параметра `cat` НЕТ.** Поэтому `initUrl: $"…/cards-{cat}.json"` даёт `cards-.json` → 404
→ `html == null` → `BadRequest("html")` → HTTP 500. Карточка **не знает категорию** — она
получает только `uri` и `type`.
**Питфолл JSONPath:** ключ `movie/424781` в `SelectToken` трактуется как **вложенность**
(`movie` → `424781`) → узел не найден → **HTTP 400 `parse.node`**. Слэш в JSONPath = разделитель.
**Итоговое решение (в YAML уже записано, деплой не доделан):**
```yaml
card_parse:
initUrl: |
return $"{host}/cards/card-{uri.Replace("/", "-")}.json";
node: card
name: { attribute: name }
original_name: { attribute: original }
year: { attribute: year }
image: { attribute: poster }
description: { attribute: overview }
```
Файлы `wwwroot/cards/card-<media>-<id>.json` (345 шт., ~1 КБ) с обёрткой `{"card": {...}}`
и фиксированным путём `$.card` — потому что `node` статичен и один общий файл по `uri`
не отфильтровать.
**Диагностика карточки (воспроизводимо):**
```bash
curl -s -o /tmp/c.json -w "HTTP %{http_code}\n" \
"http://192.168.1.14:9118/catalog/card?plugin=watchlist&uri=movie%2F424781&type=movie"
# 500 (нет card_parse) / 400 parse.node (node не нашёл узел) / 200 + JObject (ок)
```
⚠️ Ошибки карточки **в логи не пишутся** (`docker logs lampac` пуст) — диагностировать только
по HTTP-коду и телу ответа (`parse.node`, `html`, `json`).
## 🔴 ПИТФОЛЛ ДЕПЛОЯ: sudo через ssh без tty
`ssh kraken "sudo docker cp …"` падает с `sudo: a terminal is required to read the password`.
`-t` обязателен, но при этом **нельзя передавать `a && b`** — цепочка рвёт tty-сессию
(«Connection to kraken closed»). Каждую команду вызывать **отдельно**:
```python
full = f"ssh -t {KRAKEN} {cmd}" # одна команда, без &&
```
⚠️ **`docker compose up -d` НЕ пересоздаёт контейнер, если менялся только список volumes.**
Симптом: `docker inspect` не показывает новое монтирование, патч не виден.
**Фикс: `docker compose up -d --force-recreate`.** После — проверить
`docker inspect lampac --format '{{json .Mounts}}' | jq -r '.[].Destination'`.
## ✅ ДЕПЛОЙ: что залито и чем (2026-09-22, третья сессия)
**Скрипты (на Mac, `/Users/admin/lampac-tmp/`):**
| Файл | Роль |
|---|---|
| `gen_watchlist_v2.py` | генератор: `.md` → постеры/TMDB-ID/карточки (Radarr → TMDB → Кинопоиск-фолбэк) |
| `patch_plugin.py` | патч `plugin.js`: `+enumerable:true,configurable:true` в оба `defineProperty` |
| `plugin-patched.js` | результат патча (10738 Б), `node --check` чист |
| `deploy_lampac.py` | заливка JSON/YAML/plugin.js + volume + пересоздание + самопроверка |
| `watchlist.yaml` | описание каталога для Catalog (4 категории + `card_parse`) |
| `docker-compose.yml` | + volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` |
| `movies-watchlist.md` | локальная копия списка (682 строки) для прогонов |
**Кэши генератора (в `$HOME`):**
- `~/.lampac_tmdb_cache.json` — `{title|year: [tmdb_id, media_type, poster_path]}` (поиск)
- `~/.lampac_tmdb_details.json` — `{movie/ID: {overview, genres, cast, runtime, vote, …}}` (детали карточки)
**Что даёт прогон (замер):**
```
films: 360 постеров 308 TMDB-ID 312
new: 33 постеров 30 TMDB-ID 30
cartoons 3 постеров 3 TMDB-ID 3
anime: 7 постеров 7 TMDB-ID 7
источники: Radarr 9, TMDB 343, Кинопоиск-фолбэк 51, карточек 352
```
**Артефакты в `wwwroot/` (все на HDD Кра́кена):**
`watchlist.json` · `watchlist-{films,new,cartoons,anime}.json` · `cards.json` · `cards/card-<key>.json`
**Команды (порядок):**
```bash
cd /Users/admin/lampac-tmp
python3 gen_watchlist_v2.py movies-watchlist.md /tmp/wl-out/watchlist.json
python3 deploy_lampac.py /tmp/wl-out
```
⚠️ После правки генератора **перезапускать его**, а не только деплой — иначе в `wwwroot`
уедет старый формат файлов (один раз на этом потерял `cards.json` как список вместо словаря).
## 🔴 АРХИТЕКТУРНАЯ ОШИБКА 2026-09-22 — почему каталог «не работает»
@@ -800,6 +926,62 @@ curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films&page=1
## 🟢 РЕШЕНИЕ: первая категория игнорирует `format` (2026-09-22, проверено)
**Это была настоящая причина «здесь пусто» в UI.** Читать первым, если каталог отдаёт
названия, но без постеров и с `id` = ссылкой на поиск Кинопоиска.
**Симптом (замер):**
```
cat=films → 360 элементов, постеров 0, id = "index.php?kp_query=…"
cat=films&sort=rating → 1 элемент (правильный файл!)
cat=new → 33 ✅ cat=cartoons → 3 ✅ cat=anime → 7 ✅
```
Диагноз: `cat=films` читала **`watchlist.json`** (общий файл, 360 = `channels.films.items`),
а не `watchlist-films.json`. То есть **блок `format` для неё не применился**.
**Причина — по коду `ListController.cs`:**
```csharp
// строка 50: базовый URL
string url = $"{init.host}/{(page == 1 && init.list?.firstpage != null
? init.list?.firstpage : init.list?.uri)}";
// строки 59-85: ветка при непустом cat
var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat));
string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-");
if (!string.IsNullOrEmpty(eval))
url = CSharpEval.BaseExecute<string>(eval, new CatalogGlobalsMenuRoute(...));
```
Категория, **стоящая ПЕРВОЙ** в списке `categories`, до применения `format` уже получает
базовый URL из `list.uri`. Пока `Фильмы` была первой, она читала `list.uri`
(= `watchlist.json` при том значении, что стояло) вместо своего `format`-URL.
**Фикс (одна перестановка):**
```yaml
menu:
- categories:
Новинки: new # ← первой ставим НОВИНКИ
Фильмы: films # (она читает list.uri = watchlist-new.json)
Мультфильмы: cartoons
Аниме: anime
format:
"-": |
return $"http://127.0.0.1:9118/watchlist-{cat}.json";
```
⚠️ **`list.uri` и `list.firstpage` обязаны совпадать с файлом первой категории**
(здесь — `watchlist-new.json`), иначе первая категория отдаст чужой файл.
В генераторе это зафиксировано комментарием-питфоллом.
**Проверка после правки (все 4 должны дать свои числа, а не общий список):**
```bash
for c in new films cartoons anime; do
curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=$c&page=1" \
| jq -r "\"$c: \(.results|length) элементов, постеров \([.results[]|select(.img!=null)]|length)\""
done
```
## 🔴 ГЛАВНЫЙ ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН, он генерируется сам (2026-09-22)
**Симптом:** добавил блок `main:` в `sites/watchlist.yaml` → «Мой список» **полностью
+49
View File
@@ -201,6 +201,55 @@ id = href,
`http://{localhost}:{port}/tmdb/api/3/{cat}/{id}?api_key={CoreInit.conf.cub.api_key}`
— ключ уже внутри образа, отдельный TMDB-ключ для карточек не нужен.
### 🔴 Питфолл №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}` — **не зависит от содержимого файлов**.