[2026-09-22] eagle: personal/plans/lampac-watchlist-plugin.md personal/tech/lampac-deployment.md
This commit is contained in:
1 parent
bd00f6f145
commit
5e389f93d7
2 files changed
+232
-1
No files matched your search
@@ -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` → «Мой список» **полностью
|
||||
|
||||
@@ -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}` — **не зависит от содержимого файлов**.
|
||||
|
||||
Reference in new issue
Block a user