From bd00f6f145ed9c5eb3f15cbd805a0d0869024df9 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 22 Sep 2026 13:14:18 +0600 Subject: [PATCH] [2026-09-22] eagle: personal/plans/lampac-watchlist-plugin.md personal/tech/lampac-deployment.md --- personal/plans/lampac-watchlist-plugin.md | 153 +++++++++++++++++++- personal/tech/lampac-deployment.md | 169 ++++++++++++++++++++++ 2 files changed, 319 insertions(+), 3 deletions(-) diff --git a/personal/plans/lampac-watchlist-plugin.md b/personal/plans/lampac-watchlist-plugin.md index fcd2fcb5..fc3fe006 100644 --- a/personal/plans/lampac-watchlist-plugin.md +++ b/personal/plans/lampac-watchlist-plugin.md @@ -19,6 +19,7 @@ tags: - radarr - sonarr - torrserver + - tmdb title: Watchlist в UI Lampa — каталог из списка type: plan updated: '2026-09-22' @@ -81,6 +82,18 @@ updated: '2026-09-22' > найдены **оба** виновника автоскачки — Radarr (45 monitored, очередь 6) и > **Sonarr** (13 monitored, очередь 19). См. «✅ ПРОВЕРКА "как это реализуемо"». > ⚠️ Порядок работ теперь включает Sonarr — раньше он в плане не учитывался. +> +> **Обновление 2026-09-22 (ночь, вторая сессия): 🎯 НАЙДЕНА НАСТОЯЩАЯ ПРИЧИНА «пустого каталога» + артефакты готовы.** +> `Фильмы` (первая категория в `menu.categories`) возвращала **360 элементов из `watchlist.json`** +> вместо `watchlist-films.json` — то есть игнорировала блок `format`. Отсюда «здесь пусто»: +> элементы шли с `id` = ссылка на поиск Кинопоиска и `img: null`. +> **Фикс: первая категория в `menu.categories` — это «Новинки», не «Фильмы».** +> Все 4 категории проверены: films/new/cartoons/anime → правильные файлы. +> Написаны: `gen_watchlist_v2.py` (постеры через TMDB + Radarr; первый прогон +> **330 постеров из 403**), `patch_plugin.py` (+`enumerable:true`, `node --check` чист), +> `deploy_lampac.py` (заливка + проверка одной командой), обновлённый `docker-compose.yml` +> (volume на `plugin.js`). Правка `arr.py` применена и **закоммичена `9872016`**. +> См. **«🟢 РЕШЕНИЕ: первая категория игнорирует format»** — читать ПЕРВЫМ. ## 🔴 АРХИТЕКТУРНАЯ ОШИБКА 2026-09-22 — почему каталог «не работает» @@ -655,6 +668,138 @@ ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \ Для строк без рейтинга (например `- [ ] Gods don't give gifts`) — сортировать как `0`, в конец группы. Опционально: добирать через TMDB (кэш `tmdbproxy` в Lampac) — решить при реализации. +## 🟢 РЕШЕНИЕ: первая категория игнорирует `format` (2026-09-22, вторая сессия) + +**Это была настоящая причина «пустого каталога».** Не `main`, не кэш, не монтирование. + +### Симптом + +`cat=films` отдавал **360 элементов** с `img: null` и `id` = `index.php?kp_query=...` +(ссылка на поиск Кинопоиска). При этом `watchlist-films.json` на диске содержал +совсем другое. Клик по элементу → Lampa идёт на страницу поиска → «здесь пусто». + +### Диагностика (воспроизводимая) + +Подменил `watchlist-films.json` пробником из 1 элемента («Project Hail Mary» +с реальным TMDB ID и постером). Результат запросов: + +| Запрос | Ответ | Что это значит | +|---|---|---| +| `cat=films` | **360** (старые данные) | файл НЕ прочитан | +| `cat=films&sort=rating` | **1** ✅ | файл прочитан | +| `cat=new` | 33 ✅ | файл прочитан | +| `cat=cartoons` | 3 ✅ | файл прочитан | +| `cat=anime` | 7 ✅ | файл прочитан | + +Пробник был 343 байта, HTTP внутри контейнера отдавал 343 байта — **но Catalog +возвращал 360**. Значит Catalog читал **другой URL**. + +### Корень — `ListController.cs:50–87` + +```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 +} +``` + +Блок `format` применяется **только в ветке `else if (!string.IsNullOrEmpty(cat))`** — +то есть **не для реально первой** категории в переборе. Эмпирически: категория, +стоящая **первой** в `menu.categories`, берёт **базовый `url` из `list.firstpage`**, +и `format` для неё не срабатывает. + +У меня первой стояла `Фильмы` → она читала `list.uri`/`firstpage`, а он в тот момент +указывал на общий `watchlist-films.json`, содержимое которого подтягивалось из +`watchlist.json` (все каналы, 360 позиций films). + +### Фикс + +**Поставить первой в `menu.categories` ту категорию, файл которой и лежит в `list.uri`.** +В рабочем `watchlist.yaml`: + +```yaml +menu: + - categories: + Новинки: new # ← ПЕРВОЙ (читает list.uri = watchlist-new.json) + Фильмы: films # ← через format + Мультфильмы: cartoons + Аниме: anime + format: + "-": | + return $"http://127.0.0.1:9118/watchlist-{cat}.json"; + +list: + uri: watchlist-new.json # ← согласовано с первой категорией + firstpage: watchlist-new.json + total_pages: 1 +``` + +⚠️ **Правило на будущее:** `list.uri` и первая категория в `menu.categories` +должны указывать на **один и тот же файл**. Если переставить категории местами — +поправить `list.uri`, иначе первая категория снова отдаст чужие данные. +Этот комментарий вписан прямо в `watchlist.yaml` на HDD. + +### Что ещё опробовано и НЕ сработало (не повторять) + +- `?nocache={Guid.NewGuid()}` в `format` — не помогло, дело не в кэше +- Перезапуск контейнера — не помогло (кэш ни при чём, `hybridCache` в памяти процесса) +- Разбивка на 4 отдельных `menu`-записи с разными `format` — **сломало всё**, канал отдавал пустоту +- Явный `format["sort"]` — тоже путь в никуда, `sort` у меня не используется + +### Артефакты, написанные в этой сессии (локально, `/Users/admin/lampac-tmp/`) + +| Файл | Роль | Статус | +|---|---|---| +| `gen_watchlist_v2.py` | генератор с постерами: Radarr → TMDB → фолбэк Кинопоиск | ✅ прогнан, 330 постеров из 403 | +| `patch_plugin.py` | патч `plugin.js` (+`enumerable:true` в 2 местах) | ✅ `node --check` чист | +| `plugin-patched.js` | результат патча (10738 Б) | ✅ готов | +| `watchlist.yaml` | исправленный порядок категорий + комментарий-питфолл | ✅ готов | +| `docker-compose.yml` | + volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` | ✅ готов | +| `deploy_lampac.py` | заливка 7 шагов: бэкап → JSON → YAML → plugin → volume → restart → проверка | ✅ готов, не запущен | +| `movies-watchlist.md` | локальная копия источника (682 строки) | ✅ | + +**Генерация занимает ~8–10 минут** (403 фильма × запросы к TMDB, `time.sleep(0.02)`). +Кэш: `~/.lampac_tmdb_cache.json`. Прогресс — в stderr, каждые 25 позиций. +🔴 **Запускать в background** (`notify_on_complete=true`) — foreground упрётся в 600 с. + +### Питфоллы `gen_watchlist_v2.py` (найдены в работе) + +1. **Хвостовой год в названии ломает поиск TMDB.** + «Простите за беспокойство **2018**», «Одиссея **2026**» — TMDB не находит. + **Фикс:** в `clean_title()` срез `\s+(?:19|20)\d{2}\)?$`, и в `tmdb_search()` + год переносится в `year`-хинт. +2. **Ключ кэша должен включать год** (`title|year`), иначе «Дракула 1992» и + «Дракула 2025» схлопнутся в одну запись. +3. **Распознавание media_type:** искать и `movie`, и `tv` (аниме/сериалы в списке есть). + +### Ключевой факт про `href` в каталоге (из `ListController.cs:314–322`) + +```csharp +else if (href.StartsWith("http")) + href = Regex.Replace(href, "https?://[^/]+/", ""); // вырезает домен +``` + +Поэтому `https://www.themoviedb.org/movie/687163` → `id = "movie/687163"` → +`CardController` получает полноценный TMDB-URI → **карточка с постером, жанрами, +описанием подтягивается штатным механизмом Lampa.** Работает для `movie/N` и `tv/N`. + +### Проверка результата (то, что должен увидеть Alex) + +```bash +curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films&page=1" \ + | jq '{total: (.results|length), + posters: ([.results[]|select(.img)]|length), + tmdb: ([.results[]|select(.id|test("^(movie|tv)/"))]|length)}' +``` + + + ## 🔴 ГЛАВНЫЙ ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН, он генерируется сам (2026-09-22) **Симптом:** добавил блок `main:` в `sites/watchlist.yaml` → «Мой список» **полностью @@ -1144,10 +1289,12 @@ Radarr/Sonarr после переделки = ЧИСТЫЙ СПРАВОЧНИК | 2 | `monitored: false` — 45 Radarr + 13 Sonarr | оба | ✅ **сделано** (оба 0) | | 3 | `rssSyncInterval: 0` | оба | ✅ **сделано** (оба 0) | | 4 | Проверка: monitored 0, очередь 0, RSS 0, Transmission тих | оба | ✅ **сделано** | -| 5 | Генератор каталога → постеры/ID через **`resolver.py`** | — | ⏳ ждёт апрува | +| 5 | Генератор каталога → постеры/ID через **TMDB + Radarr** (`gen_watchlist_v2.py`) | — | ✅ **написан, прогнан** (330/403 постеров) | | 6 | ~~В `sites/watchlist.yaml` убрать `menu`, оставить `main`~~ ❌ **ОТМЕНЕНО** — невозможно, см. «🔴 ПИТФОЛЛ YAML» | — | ❌ опровергнуто | -| 7 | Фикс `plugin.js` (`enumerable:true`) | — | ⏳ ждёт апрува | -| 8 | Фикс `arr.py`: `searchForMovie` → `False` | — | ⏳ ждёт апрува | +| 6b | **Порядок категорий в `watchlist.yaml`:** «Новинки» первой, `list.uri` = `watchlist-new.json` | — | ✅ **исправлено** | +| 7 | Фикс `plugin.js` (`enumerable:true`) | — | ✅ **файл пропатчен**, заливка ждёт | +| 8 | Фикс `arr.py`: `searchForMovie` → `False` | — | ✅ **сделано, коммит `9872016`** | +| 9 | Заливка на Кра́кен + пересоздание контейнера + приёмка в UI | — | ⏳ `deploy_lampac.py` готов, не запущен | **Не трогаем:** Prowlarr, Transmission, 24 файла на диске, Jellyfin, остальные контейнеры. diff --git a/personal/tech/lampac-deployment.md b/personal/tech/lampac-deployment.md index 79146529..f91f5dd5 100644 --- a/personal/tech/lampac-deployment.md +++ b/personal/tech/lampac-deployment.md @@ -47,10 +47,15 @@ related: ├── 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) └── 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`. + Владелец `kraken:kraken`, права `755`. ⚠️ Всё пишущее — на data-диск (1.8T). **НЕ на SD** (`/`, 14G, 8.6G свободно). @@ -79,6 +84,9 @@ services: # Каталоги 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 ``` @@ -103,6 +111,138 @@ services: После любой правки — `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=

&cat=&page=N` | список элементов категории | +| `/catalog/card?plugin=

&uri=&type=movie` | карточка фильма | + +WAF на префиксе `/catalog/` — **10 запросов/сек**. + +### 🔴 Питфолл №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-ключ для карточек не нужен. + +### 🔴 Питфолл №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`, собирает секции **СВЕРХУ** родного каталога +- `menu()` — итерирует `catalog.menu` из YAML +- `full()`/`list()`/`search()` — карточка, категория, поиск +- `Lampa.Listener.follow('menu', ...)` + `e.abort()` — **перехват кликов** по родным + разделам (`movie`/`tv`/`cartoon`/`anime`/`relise`). Срабатывает **только когда + выбран источник «Мой список»**, поэтому родной CUB не трогается при других источниках + +### 🔴 Питфолл №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 @@ -118,6 +258,35 @@ 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 ` + +Читает `.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