diff --git a/personal/plans/lampac-watchlist-plugin.md b/personal/plans/lampac-watchlist-plugin.md index fc3fe006..7fa603ef 100644 --- a/personal/plans/lampac-watchlist-plugin.md +++ b/personal/plans/lampac-watchlist-plugin.md @@ -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=&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 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--.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-.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(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` → «Мой список» **полностью diff --git a/personal/tech/lampac-deployment.md b/personal/tech/lampac-deployment.md index f91f5dd5..fff0ad60 100644 --- a/personal/tech/lampac-deployment.md +++ b/personal/tech/lampac-deployment.md @@ -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 Index(string plugin, string uri, string type) +``` +🔴 **Параметра `cat` НЕТ.** Карточка знает только `uri` и `type`. Поэтому +`initUrl: $"…/cards-{cat}.json"` даёт `cards-.json` → 404 → HTTP 500. + +**Что можно и что нельзя подставлять:** + +| Поле | Механика | Подстановка | +|---|---|---| +| `initUrl` | `CSharpEval.Execute` с `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--.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}` — **не зависит от содержимого файлов**.