--- created: '2026-09-22' namespace: personal related: - '[[personal/plans/lampac-kraken]]' - '[[personal/tech/lampac-deployment]]' - '[[family/projects/watchlist-sync]]' - '[[family/projects/watchlist-automation]]' - '[[family/documents/movies-watchlist]]' - '[[personal/plans/lampac-watched-to-watchlist]]' status: sync-down-feed-implemented-matcher-parity-blocked tags: - lampac - lampa - watchlist - catalog - webos - plugin - kraken - radarr - sonarr - torrserver - tmdb - card-parse - feed - matcher-parity title: Watchlist в UI Lampa — каталог из списка type: plan updated: '2026-09-24' --- # Watchlist в UI Lampa — каталог из списка ## 🔴 2026-09-24 (вечер): ПЕРЕПИСЫВАНИЕ — один sync-скрипт вместо отдельного генератора **Требование Alex:** «перепиши по нормальному чтобы не отдельный скрипт на лампу был а один sync скрипт и писал сразу lampa feed… обнови синк скрипт чтобы писал фид вместо добавления закачек в arr, а отдельный lampac script удали». **Причина (найдено в этой сессии).** `gen_watchlist.py` (Lampac, 878 строк) — ВТОРОЙ, независимый матчер TMDB: он не читает ни `.resolve_cache.json`, ни маркеры ✅/🗓️/❓. Годами его связывал с резолвером Radarr неявно (матч по названию → те же версии фильмов, что выбрал `resolve`). После удаления контента из arr 2026-09-24 мост исчез, и каталог впервые матчил вслепую → 3 карточки сменили фильм (Олдбой 2003→2013 ремейк, Unbroken 2003→Несломленный 2014, Shogun 1968→Убийца сёгуна 1980). Лечим не годами в `.md`, а слиянием матчеров. **Статус: ✅ РЕАЛИЗОВАНО 2026-09-24 (вечер).** Отказ от отдельного `feed.py`/CLI-шага — Alex уточнил требование: «это же чистая доработка sync-down?» → **`sync-down` сам пишет фид**. Отдельного шага `feed` в CLI и в nightly НЕ появилось. | Что | Как сделано | |---|---| | Новый `watchlist_sync/sync_down.py` | ~380 строк вместо 195: `resolve` → `wwwroot/watchlist.json` + `watchlist-.json` + `cards/card-.json` + `cards.json` + `missing.json`; опционально ❓-синк | | CLI | `sync-down [--apply] [--feed-root]`; `--batch` оставлен deprecated (в фид идёт весь список) | | Пути | `--feed-root` → env `LAMPAC_ROOT` → `/../lampac` (fallback для Мак-чекаута). Хардкода UUID-путей в коде нет | | `arr.py` | 🔴 **УДАЛЁН** (`git rm`, коммит `a524f5d`) — история сохранена в гите. Бэкапы НЕ делались (решение Alex: «в истории гита останется») | | ⬇️ | **не ставится больше нигде.** Alex: «⬇️ больше ставить не надо, ведь закачки нет». `mark_queued` в `parser.py` оставлен для исторических строк | | Коммит | `a524f5d` — «sync-down: build Lampa feed instead of adding to arr; drop arr.py» | **🔴 Три открытия при переносе (стоили трёх итераций — не повторять):** 1. **Фильтры рейтинга/ужасов в фиде НЕ НУЖНЫ.** Я притащил в фид правила закачки (`rating >= 6.0`, horror-skip) — фид упал до 268 позиций из 392. Это правила **Radarr** («не качаем то, что не смотрим»), а каталог — не очередь закачки: старый генератор отдавал horror тоже (Вивариум, Субстанция, Ужасающий, Зубастики есть в живом фиде). В `sync_down.py` оставлен явный комментарий с запретом возвращать эти фильтры. 2. **`item.ambiguous` (❓) НЕ исключает позицию из фида.** ❓ означает «название в `.md` неточное, пригодился бы год», а НЕ «нерезолвлено». Резолвер для таких всё равно отдаёт кандидата, и старый генератор их публиковал (Альфа, Кибердеревня, Орвилл, Ножницы — все с постерами в живом фиде). Из фида исключают только: нет `tmdb_id` и `result.year_ambiguous` (🗓️ — реально два разных фильма). 3. **Фид должен брать лучшего кандидата, а не отказываться.** `resolve()` при неоднозначности возвращает `status="ambiguous"`, `candidate=None`, но топ-3 лежат в `all_candidates` — среди них верный. Старый генератор брал первый результат TMDB. Если строить фид строго по `result.status == "ok"`, теряется **54 позиции** (девять, Мементо, Babylon, Orville, Матрица-класс названий: Казино, Сияние, Оно, Отступники, Бешеные псы…). ✅ **РЕШЕНО 2026-09-24:** Alex — «бери лучшего кандидата и пиши вопрос». Фид берёт `all_candidates[0]`, а название уходит в `missing` → ❓ в `.md` (год, добавленный после ❓, пиннит позицию на следующем прогоне). Реализовано в `sync_down.py`. **🔴 ГЛАВНЫЙ БЛОКЕР, вскрытый переносом (не закрыт):** резолвер проекта матчит **хуже** старого генератора. Сверка по TMDB-номеру (не по названию — в фиде имена из TMDB, в `.md` из строки, сравнение по имени бесполезно): ``` было в живом фиде, в новом с ДРУГИМ tmdb_id: 86 позиций примеры: Злая: Часть 2, Разделение, Дракула, Ван Хельсинг, Судья Дредд, Дюна-класс плюс мусорные матчи по кривым строкам: «[Убийственная вечеринка] (2025)» → фильм 2007 ``` **Причина:** в старом генераторе 4 ступени резолва — Radarr → TMDB → **КП-по-номеру из ссылки `kinopoisk.ru/film/N/`** → КП-по-названию. Самая надёжная ступень (номер из ссылки — машинный ключ, не зависит от переименований) в `resolver.py` **отсутствует вообще**. До переноса это маскировал Radarr-фастпатч; после удаления контента arr маскировка исчезла и расхождение вылезло. **Варианты (ждёт решения Alex):** - **А (рекомендован):** перенести в `sync_down`/`resolver` ступень «КП-по-номеру из ссылки» — код разобран и лежит в бэкапе `gen_watchlist.py` (`kp_search_by_id` + `_docs_to_tmdb` с `_by_id`). ~30 мин, проверяется тем же diff по tmdb_id. - **Б:** выкатить как есть и вычистить 86 расхождений руками — вернутся после прогона. **Замеры dry-run по ходу работы:** 268 (с фильтрами) → 332 (без фильтров, `ambiguous` исключается) → **397** (`all_candidates[0]` + ❓; больше 392, потому что старый генератор терял часть позиций на Radarr-фастпатче и ❓-строках). На живой фид НЕ выкатывалось: сначала matcher-parity. **Реперные метрики живого фида на 2026-09-24** (для сверки после переноса): ``` каналы: new 32 / films 346 / cartoons 7 / anime 7 → карточек 392 источники: Radarr 0, TMDB 371, автопоиск по названию 17, по номеру из ссылки 4, без TMDB 30, постеров 390 ``` 🟡 **Рабочее правило (Alex):** код писать сразу в проекте на Маке (`~/Developer/watchlist-sync/`), `/tmp` — только для чтения чужих файлов и одноразовых выборок. Мак-репа и Кракен держатся на одном коммите (`4266e37` на момент старта). **Границы (что не трогаем):** `catalog/sites/watchlist.yaml` (контракт Lampac: `plugin: watchlist`, категории, `card_parse`/`card_args` с `format`-выражениями), `catalog/plugin.js` (инжект секций в CUB через `Api.partNext`), структура `cards/card-movie-NNN.json`. Механика `missing.json` → ❓ осталась, переехала внутрь `sync_down.py` (`_sync_question_marks`, двусторонняя — снимает протухший ❓). **🔴 Незакрытое на конец сессии 2026-09-24 (для следующей сессии):** 1. **Matcher-parity (ГЛАВНОЕ)** — см. «ГЛАВНЫЙ БЛОКЕР» выше: перенести ступень «КП-по-номеру из ссылки» в резолв фида (вариант А) либо вычистить 86 расхождений руками (вариант Б). Без этого выкат на живой фид покажет не те фильмы на ~86 позициях. 2. Возврат `sync-down` в `watchlist-nightly.sh` (строка 40 сейчас «ПРОПУЩЕН»; в 02:30 крутится отдельный `watchlist-catalog.sh`) + удаление `watchlist-catalog.sh` и строки крона `30 2 * * *`. 3. Удаление `lampac/scripts/gen_watchlist.py` (878 строк) и `mark_missing.py` (179). 🔴 НЕ удалять до закрытия п.1: оттуда берётся код `kp_search_by_id` / `_docs_to_tmdb`. 4. Прогон `--apply` на живой фид + `docker restart lampac` (сброс кэша Catalog) + приёмка в UI Lampa. 5. Правка `sync_down.py`: ветка «не нашлось» (`not_found`) обязана добавлять позицию в `missing` — за это отвечает `_sync_question_marks` (двусторонний ❓). --- > Реализовано 2026-09-22. План был переписан после разведки кода модуля Catalog. > Базовое развёртывание — [[personal/plans/lampac-kraken]] (шаги 1–6 закрыты). > > 🔴 **АРХИТЕКТУРА ПРИЗНАНА НЕВЕРНОЙ 2026-09-22 — переписывается.** > Сделано `menu` (подмена родных разделов) вместо `main` (секции СВЕРХУ). > Плюс источник данных — парсинг текстового `.md` вместо **Radarr API**, где уже > есть TMDB ID и постеры. Alex: «Я предполагал что будет отображаться то что и раньше > (список cub) но В НЕГО будет сверху добавлено несколько секций в которых будут > РЕАЛЬНЫЕ ЭЛЕМЕНТЫ - а не сгенерируют пустые заглушки!» > См. «🔴 АРХИТЕКТУРНАЯ ОШИБКА» — читать ПЕРВЫМ. > > **Итог одной строкой:** серверная часть каталога работает полностью — «Мой список» > первым в `/catalog`, 4 категории (403 позиции, 348 постеров), **карточка по клику > отдаёт все поля: 352/352 с числовым `tmdb_id`**. Родной CUB не тронут. > Осталась приёмка глазами на ТВ. > > **Обновление 2026-09-22 (четвёртая сессия): 🔴🔴 НАЙДЕНА ИСТИННАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО» — КИРИЛЛИЦА В КЛЮЧЕ КАТАЛОГА.** > Все предыдущие фиксы (числовой `tmdb_id`, права, macOS-tar) были **не той причиной**. > `tcpdump` на Кра́кене при клике на ТВ показал реальный запрос: > `GET /catalog/card?plugin=%D0%9C%D0%BE%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA&uri=... → 400 Bad Request`. > **`plugin=Мой список`** — кириллица. `ModInit.goInit` строки 51-52 чистит имя регуляркой > `[^a-z0-9\-]`, то есть **вырезает всю кириллицу целиком** → `""` → `File.Exists("sites/.yaml")` = false → > fallback `FindSiteByDisplayName("")` тоже мимо (сравнивает уже с пустой строкой) → `null` → > `BadRequest("init not found")` → **400** → «Здесь пусто». > **ФИКС:** явный `plugin: watchlist` в `watchlist.yaml` (переопределяет ключ по > `ApiController.cs:136`); `displayname: Мой список` остаётся надписью в UI. Ключ каталога стал `watchlist`. > Проверено: `plugin=Мой список` → **400**, `plugin=watchlist` → **200** с полными данными. > 🔴 **Урок диагностики:** проверять карточку curl'ом надо ТЕМ ЖЕ ключом, который шлёт ТВ > (я подставлял латиницу руками → 200 → ложный вывод «проблема в клиенте»). Детали — > [[personal/tech/lampac-deployment]] раздел «🔴🔴 ГЛАВНАЯ ПРИЧИНА». > > **⚠️ ЗАДАЧА СЕССИИ (требование Alex): ✅ ВЫПОЛНЕНО — СЕКЦИИ ВНЕДРЯЮТСЯ В CUB-ФИД.** > Дословно требование: «надо блядь добавить в CUB список новые секции а не ручное > блядь переключение!». Реализовано в `plugin-cub.js` через **обёртку > `Lampa.Api.sources.cub.main`**: наши секции **добавляются** в массив > `parts_data` CUB-фида (не подменяют раздел), карточкам проставляется > `source: 'watchlist'`. Вызов — по событию `app:ready`. > Залито, отдаётся в сборке `/catalog.js` (35566 Б). **Ожидает приёмки в UI.** > Механика — [[personal/tech/lampac-deployment]] раздел «Внедрение секций Watchlist в CUB-фид». > > **Обновление 2026-09-22 (ВОСЬМАЯ СЕССИЯ): ✅✅ ЭТАП «СЕКЦИИ ВНУТРИ CUB» ЗАКРЫТ — ФИД СМЕРЖИЛСЯ.** > Alex: «Отлично, фид смержился. Закрываем этот этап в плане.» > > **Корень, который держал этап** (нашёл только по коду `app.min.js` @1292588): > ```js > function partNext(parts, parts_limit, partLoaded, partEmpty) { > var pieces = parts.filter(function (p) { > return typeof p == 'function'; > }).slice(0, 0 + parts_limit); // ← limit = 6, ЖЁСТКИЙ СРЕЗ ГОЛОВЫ > ``` > `cub.main$1` кладёт в `parts_data` **28 родных секций** и вызывает `partNext(parts, 6, …)`. > Наш инжект делал `parts.push()` → секции уходили в **хвост** (позиции 29–32) → `slice(0, 6)` > их отрезал. Трасса пула на живом ТВ: `before: 28 → after: 32`, в DOM — **ни одной** нашей секции. > > **ФИКС: `parts.unshift()` вместо `parts.push()`** — в голову пула, в срез, рисуются СВЕРХУ > (ровно требование Alex). Порядок меню обходится **в обратном порядке**, потому что каждый > `unshift` сдвигает предыдущие вправо. > > **Ложный след, потративший время:** тест `g: {wlSection: "undefined", …}` читался как > «функции недоступны из замыкания». На самом деле инжект **внутри** замыкания плагина и > `wlSection` виден перехватчику — проба смотрела снаружи. Доказательство: прямой вызов > `partNext(empty,…)` дал `len: 4`. **Проверять пул ТРАССОЙ (`before/after`), а не наличием > функций на `window`.** > > **Приёмка (серверная часть): 18/18.** `/catalog.js` 33550 Б с `unshift`; `/catalog` → > ключ `watchlist`, menu 4 категории; список: new 33/30 постеров, films 360/308/312 ID, > cartoons 3/3, anime 7/7; карточка `tmdb_id: 424781` числом; регресс-тест `plugin=Мой список` > → **400** (кириллица по-прежнему не проходит). > > **Артефакты:** `build_plugin.py` (+инварианты `wlInject def`, `unshift (не push)`, > `push в INJECT (должно 0)`), `verify_unshift.py` (проверка фикса в рантайме БЕЗ деплоя), > `full_check.py` (полный цикл сервер+ТВ), `tv_accept.py`, `diag_trace.py`, `diag_calls.py`. > Бэкапы: `plugin.js.bak-20260922-165613` (последний рабочий), `plugin-cub.js.bak-*`. > > 🔴 **Питфолл рантайм-проверки:** повторная загрузка `/catalog.js` скрипт-тегом НЕ перезапускает > `startPlugin` (флаг `window.plugin_catalog` уже стоит) → `patched: false`. Для быстрой итерации > без деплоя оборачивать `Lampa.Api.partNext` вручную с `unshift` (`verify_unshift.py`). > `location.reload()` на webOS догружает новый `catalog.js` **дольше 30 с** — ждать до 120 с. > **Обновление 2026-09-22 (седьмая сессия): 🔴🔴 ПРИЧИНА «В CUB НИХУЯ НЕ ПОМЕНЯЛОСЬ» — `Manifest` НЕДОСТУПЕН В ОБЁРТКЕ.** > Alex: «Ты вообще нихуя не поменял!!». Проверка через **CDP-пробу на живом ТВ** подтвердила > его правоту: обёртка `Api.sources.cub.main` стояла (`__wl_injected: true`), но вызов > `cub.main({})` возвращал **4 родные секции** и **ни одной нашей**. > **Корень:** первая версия инжекта читала `Manifest.catalogs['watchlist']` напрямую, а > `Manifest` объявлен **локально в замыкании плагина** → `ReferenceError: Manifest is not defined` > → `catch` → молча отдавался родной фид. **Ошибки нет ни в UI, ни в логах.** > **Фикс:** `window.__wl_menu` (заполняется в `loadCatalogs`) + `wlEnsureMenu()` с фолбэком > на запрос `/catalog`. Переписан сборщик: **`build_plugin.py`** (детерминированный, > с инвариантами `INJECT=1`/`def=1`/`startPlugin=1`, `node --check` + баланс скобок), > вместо неидемпотентного `patch_cub_inject.py` (он вставлял INJECT дважды и ломал скобки). > Залито (сборка **35440 Б**), **ждёт перезапуска Lampa на ТВ** — `docker restart lampac` > НЕ перезагружает плагин в памяти ТВ. > Механика + CDP-пробы — [[personal/tech/lampac-deployment]] раздел > «🔴🔴 ШЕСТАЯ СЕССИЯ: ИСТИННАЯ ПРИЧИНА…». > **Обновление 2026-09-22 (пятая сессия): ✅✅ ПРИЁМКА ALEX ПРОЙДЕНА — ФИЛЬМЫ ОТКРЫВАЮТСЯ И ПРОИГРЫВАЮТСЯ.** > Дословно: «фильмы из списка Watchlist теперь открываются и проигрываются». > Закрыт последний блокер — кириллический ключ плагина (`plugin: watchlist`). > Плюс устранён питфолл `Staticache` (кэш собранного `/catalog.js` переживал > заливку и выглядел как «патч не применяется» → `docker restart lampac`). > > **Осталось (2026-09-22, конец СЕДЬМОЙ сессии):** > 0. ✅ **ЗАКРЫТО 2026-09-22 (восьмая сессия):** `plugin-cub.js` залит на Кра́кен, > фикс `unshift` вместо `push` — **фид смержился**, секции Watchlist рисуются > внутри главной CUB рядом с родными. См. блок «ВОСЬМАЯ СЕССИЯ» выше. > 1. **5 фильмов с Кинопоиск-ID** (`film/NNNNN/`) — «Рассекая волны: экспедиция „Лас Бальсас“», > «Хамнет», «Эхопраксия», «Цена вершины», «Астрал. Могильный ритуал 2», «О добре и зле», > WeWork (2024). > TMDB не нашёл (2025 год) → нет карточки и постера → клик даёт «здесь пусто». > **Нужен выбор Alex:** заглушка с кнопкой поиска торрента либо дорисовать `tmdb_id`. > 2. **~51 название без постера** — проблема ЗАПИСИ В СПИСКЕ, не скрипта: > `Плохие парни 1/2` (ДВА фильма одной строкой), `Солярис Тарковский` (уточнение > режиссёра), `Самолетом, поездом, машиной (1987)` (русский аналог). > Генератор такое угадать не может — **правится только список руками**. > > **Обновление 2026-09-22 (четвёртая сессия, ранее): ✅ ЧИСЛОВОЙ `tmdb_id` В КАРТОЧКАХ.** > 352 из 352 карточек отдают `tmdb_id` числом, `source`, `runtime`, `vote_average`, жанры. > Механика: `ModInit.cs:285 nodeValue` возвращает любой `JValue` СТРОКОЙ → в `card_args` > нужен `format: "long.TryParse(value, out long v) ? v : 0"` (`format` = CSharpEval, > `CatalogNodeValue(value, host)`). Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ и > переживает `docker restart` — после правки данных обязателен рестарт И проверка на > НЕзакэшированном uri. Детали — [[personal/tech/lampac-deployment]]. > > **Обновление 2026-09-22 (третья сессия): ✅ КАРТОЧКА ПОЧИНЕНА ОКОНЧАТЕЛЬНО.** > «Здесь пусто» давало отсутствие **числового** `tmdb_id`: `plugin.js` делает > `if(json.tmdb_id)`, а `ModInit.cs:285` возвращает любое `JValue` СТРОКОЙ. > Фикс — `format: "long.TryParse(...)"` в `card_args`. Плюс кэш `catalog:card:*` > в ПАМЯТИ переживал `docker restart` и отдавал старое. См. «🔴 ФИНАЛЬНЫЙ ФИКС». > > **Обновление 2026-09-22 (вечер):** Alex лично нашёл источник в UI > («остальное → основной источник → Мой список»), увидел результат и подтвердил > диагноз: секции подменяют CUB, нет превью, элементы — сырые строки файла. > Затем сам сформулировал целевую схему (Radarr = источник метаданных **без автоскачки**, > просмотр через TorrServer, файлы на HDD не пишутся) → записано в **«ПЕРЕДЕЛКА 2»**. > План работ на апруве, **ничего не выполнено**. > > **Обновление 2026-09-22 (поздний вечер): ✅ ШАГИ 0–4 ПЕРЕДЕЛКИ 2 ВЫПОЛНЕНЫ.** > Автоскачка на HDD **выключена фактически**: Radarr (45→**0** monitored, очередь 6→**0**, > RSS 30→**0**) и Sonarr (13→**0**, 19→**0**, 15→**0**). Метаданные целы — 50 фильмов, > **46 с постерами**, 24 файла на диске не тронуты. Проверено: Transmission не сдвинулся > ни на байт за 45 с, логи Radarr пусты. Бэкапы `radarr.db`/`sonarr.db`/`config.xml` — метка > `20260922-123404`. См. «✅ Порядок работ» и «🔴 Питфоллы выполнения». > > **Обновление 2026-09-22 (ночь): 🔴 ПОПЫТКА УБРАТЬ `menu` ПРОВАЛЕНА — ГИПОТЕЗА БЫЛА НЕВЕРНА.** > Попытка «оставить `main`, убрать `menu`» **сломала канал**: «Мой список» исчез из `/catalog`, > `catalog/list?plugin=watchlist` отдавал пустоту. Три раунда правок YAML, три рестарта. > **Установлено тестом:** > - **`menu` ОБЯЗАТЕЛЕН** — он регистрирует канал, а не подменяет разделы > (`ApiController.cs:70`: без `menu` сайт отбрасывается). Убрать нельзя. > - **блок `main` в YAML НЕ СУЩЕСТВУЕТ** — любые формы (список/словарь) ломают парсинг, > `goInit` → null, сайт молча пропадает. **Errors в логах нет.** > - **`main` Catalog генерирует САМ** из `menu` — в JSON-ответе `/catalog` есть оба ключа > с одинаковым содержимым. > - **Подмену разделов делает `plugin.js`**, не YAML (`Listener.follow('menu')` + `e.abort()`), > срабатывает только при выбранном источнике «Мой список». > - **`host` + `uri` СКЛЕИВАЮТСЯ** (`ListController.cs:53`) → `uri` обязан быть относительным. > Также найден питфолл: **явный `format` в `menu` перезагружает страницу** при выборе раздела. > > **Текущее состояние:** каталог **рабочий** (waтchlist.yaml откачен к рабочей форме, > «Мой список» на месте, 4 категории 360/33/3/7). Правки 5–8 (постеры через `resolver.py`, > `plugin.js`, `arr.py`) **не начаты** — ждут решения Alex по вопросу > «CUB и Мой список одновременно». См. **«🔴 ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН»**. > > **Аудит «как это реализуемо» — ✅ ЗАВЕРШЁН 2026-09-22.** Проверено всё: > TorrServer **на диск не пишет** (`UseDisk: false`, RAM-кэш 96 МБ) — главный вопрос закрыт; > поиск торрентов работает через **Jackett-API самого Lampac** (отдельный Jackett не нужен); > найдены **оба** виновника автоскачки — 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 (ночь, третья сессия): ✅ ДЕПЛОЙ ПРОШЁЛ, ПОСТЕРЫ И ПОИСК В 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»**. > **Обновление 2026-09-22 (ночь, четвёртая сессия): ✅ КАРТОЧКА ПОЧИНЕНА. 339/339 HTTP 200. СЕРВЕРНАЯ ЧАСТЬ ЗАКРЫТА.** > Схема «файл на фильм» залита и **проверена на живом сервере**: > - `wwwroot/cards/card-movie-NNN.json` (345 файлов), обёртка `{"card": {...}}`, > владелец `1000:1000`, режим `644`; > - `card_parse.initUrl` = C# `return $"{host}/cards/card-{uri.Replace("/", "-")}.json";`, > `node: card` (статичный — потому и нужен файл-на-фильм); > - **все 339 карточек с TMDB-ID отдают HTTP 200**, имена читаются > («Я буду жить», «На цепи», «Простите за беспокойство»); > - `deploy_lampac.py` дополнен шагом **2b** (заливка каталога `cards/` через tar) > и проверкой карточек в шаге 7. > > **Итог каталога:** Новинки 33/30, Фильмы 360/308, Мультфильмы 3/3, Аниме 7/7 — > **403 позиции, 348 постеров, 352 рабочих карточки**, постеров-подложек нет. > > 🔴 **Три причины поломки, все устранены** (детали — «🔴 ПИТФОЛЛЫ, найденные по ходу»): > 1. **права**: tar распаковывал от root → `root:root 600` → dotnet `Permission denied` → > статика молча **404** (выглядит как «файла нет»); > 2. **macOS AppleDouble**: BSD `tar` клал `._*.json` в `wwwroot` → нужен `--no-mac-metadata`; > 3. 🔴 **ложный диагноз — потерянный час**: карточку проверял по **`/card`**, где всегда 404. > Правильный путь — **`/catalog/card?plugin=watchlist&uri=movie/NNN`**. > > **ОСТАЛОСЬ: приёмка глазами на ТВ** (`192.168.1.70`): секции сверху с постерами, > клик → карточка с описанием → воспроизведение через TorrServer. > **Приёмка = результат виден в UI**, не «API отвечает». > > **Не тронуто, ждёт решения Alex:** 310 фильмов из списка, которых нет в Radarr > (51 без постера, идут через фолбэк Кинопоиска). Без апрува не делать. --- ## 🔴🔴 СЕДЬМАЯ СЕССИЯ: РАБОЧИЙ МЕХАНИЗМ НАЙДЕН — ПЕРЕХВАТ `Lampa.Api.partNext` **Статус: ✅ секции Watchlist рендерятся ВНУТРИ главной CUB. Проверено на живом ТВ через CDP.** Alex: «В cub секции сейчас смотрят, рекомендуем, последнее добавленное, огонь, в высоком качестве. Все.» и «до сих пор хамнет, рассекая волны, эхопраксия, WeWork итд — без постера и "здесь пусто"» + «Ты вообще нихуя не поменял!!». ### Итог проверки на ТВ (CDP, `probe_ui.py`) ``` ЛЕВО (1) → главная CUB, activity: "main" titles: ["Простите за беспокойство", "Смерть единорога", "Рассекая волны: экспедиция «Лас Бальсас»", "Я ругаюсь", "F1", "Мастерская Сатаны", "Хамнет: История, вдохновившая «Гамлета»", ..., "Эхопраксия", "WeWork создание и крах компании", ...] ``` То есть **наши фильмы физически в DOM главной CUB** — те самые, что Alex называл. ### Три бага подряд — почему «ничего не поменялось» **❌ Баг 1 (главный): обёртка `Api.sources.cub.main` не работает В ПРИНЦИПЕ.** Родной `main$1()` (в `app.min.js`, @1257163) внутри замыкания Lampa зависит от **локальных** `permit`, `get$7`, `Lang`: ```js function main$1() { var parts_limit = 6; var parts_data = [ function (call) { get$7('?sort=now_playing', params, ...); }, // ← get$7 локальная function (call) { if (permit.child_small) return call(); ... } // ← permit локальная ]; ... } ``` При вызове извне замыкания (`cub.main({})`) он падает на `permit.child_small` (`permit` → **undefined** в глобальном контексте) и **никогда не зовёт callback** → вечный `pending`. Проверено CDP-пробой: `{"pending":true}` через 15 секунд, ни одного fetch. **Обёртка мертва by design** — не потому что код плохой, а потому что метод невызываемый извне. **❌ Баг 2: `Manifest` / `Utils` / `network` недоступны вне замыкания плагина.** Замер через CDP (`probe_globals.py`): ```json {"Manifest_global":"undefined","Lampa":"object", "Utils":"undefined","network":"undefined"} ``` Все три — **локальные переменные плагина**. Первая версия инжекта читала `Manifest.catalogs['watchlist']` напрямую → `ReferenceError` → `catch` → молча отдавался родной фид. **Ошибки нет ни в UI, ни в логах.** **❌ Баг 3: `Utils.account()` без `Manifest.api_host` подставляет `{localhost}`.** ```js Lampa.Utils.account(menu[name]) // menu[name] = "/catalog/list?plugin=watchlist&cat=new" ``` → `{localhost}/catalog?uid=...&token={token}` → в логе ТВ: ``` Request GET time: 4ms error of {localhost}/catalog?uid=...&token={token} : Нет подключения к сети ``` `Utils.account` **требует** `Manifest.api_host` в области видимости. Вне замыкания его нет. ### ✅ РЕШЕНИЕ: перехват `Lampa.Api.partNext` `partNext` — **глобальный** API (`Lampa.Api.partNext`), которым сам CUB собирает главную: ```js function partNext(parts, parts_limit, partLoaded, partEmpty) { var pieces = parts.filter(function (p) { return typeof p == 'function'; }) .slice(0, 0 + parts_limit); ...Progress().append(pieces).start(...) } ``` CUB передаёт туда `parts_data` — **массив функций-секций**. Перехватив `partNext`, наши секции добавляются **в тот же пул**, что и родные: работают на любой странице, независимо от источника, без `permit` и без `params`. ```js function wlPatchPartNext() { var orig = Lampa.Api.partNext; 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); }; } ``` Плюс обязательные спутники (глобальные только!): - `wlHost()` — `Lampa.Manifest.api_host`, фолбэк хардкод `192.168.1.14:9118` - `wlAbs(url)` — склейка host + относительный путь **вручную**, `Utils.account` НЕ использовать - `wlGet(url, ok, fail)` — `new Lampa.Reguest().silent(absoluteURL, ...)`, фолбэк `fetch` - `window.__wl_menu` — заполняется в `loadCatalogs` при загрузке `/catalog`; фолбэк — сетевой запрос ### Сборщик `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 # обязательная проверка ``` Инварианты на выходе (скрипт сам их проверяет и падает при нарушении): `INJECT=1`, `def wlPatchPartNext=1`, `startPlugin=1`, `network.silent в INJECT=0`. 🔴 **Почему не `patch_cub_inject.py`:** он был неидемпотентен — при повторном запуске вставлял INJECT **дважды** и ломал баланс скобок (`SyntaxError: Unexpected token '}'`), потому что проверки шли по уже модифицированной строке. `build_plugin.py` режет файл детерминированно: `[до startPlugin] + INJECT + [новый startPlugin] + '})();'`. ### 🔴 CDP-пробы на живой ТВ (новый инструментарий) ТВ LG отдаёт **Chrome DevTools Protocol** на порту **9998**: ```bash curl -s "http://192.168.1.70:9998/json/list" | jq -r '.[] | select(.title=="Lampac") | .webSocketDebuggerUrl' # ws://192.168.1.70:9998/devtools/page/ ``` Клиент на Mac — `websocket-client` (модуль `websockets` НЕ установлен): ```python import websocket ws = websocket.create_connection(WS_URL, timeout=25) ws.send(json.dumps({"id":1,"method":"Runtime.evaluate", "params":{"expression":EXPR,"returnByValue":True,"awaitPromise":True}})) ``` Скрипты в `/Users/admin/lampac-tmp/`: | Скрипт | Что проверяет | |---|---| | `probe_tv.py` | базовое состояние: `source`, `hasCub`, `cubKeys`, `wlInjected`, `sourcesList` | | `probe_globals.py` | доступность `Manifest`/`Utils`/`network` в глобальном контексте | | `probe_cache.py` | откуда ТВ берёт `catalog.js` (script-теги, кэш) | | `probe_cub_main.py` | что отдаёт обёрнутый `cub.main` (сколько секций) | | `probe_inject.py` | инжект нового `plugin-cub.js` в рантайм БЕЗ деплоя (быстрая итерация) | | `probe_wait.py` | ждёт завершения вызова + перехватывает `console.log` | | `probe_ui.py` | **финальная приёмка:** переход на главную + чтение заголовков из DOM | | `restart_lampa.sh` / `tv_restart.sh` | перезапуск Lampa на ТВ через `luna-send` | 🔴 **Питфолл `luna-send` через ssh:** аргументы JSON не проходят через `ssh 'script -q -c "luna-send ... -a {\"id\":...}"'` — luna-send печатает help («send specified appId in message (default is none)»). **Фикс:** положить скрипт на ТВ файлом (`scp tv_restart.sh`) и вызвать `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"}' ``` 🔴 **`eval()` не делает функции глобальными.** Инжект `eval(code)` внутри IIFE плагина оставляет `wlHost`/`wlAbs` в замыкании → снаружи (`wlHost()`) `ReferenceError`. Это нормально: `cub.main` их видит, внешние пробы — нет. Проверять только через вызов `Lampa.Api.main()` или чтение DOM. ### Перезапуск Lampa на ТВ — обязателен 🔴 **`docker restart lampac` НЕ перезагружает плагин в памяти ТВ.** `catalog.js` закэширован браузером webOS. Признак «ТВ видит старую версию»: `String(Lampa.Api.sources.cub.main)` содержит старый код. Порядок: залить `plugin.js` на Кра́кен → `docker restart lampac` (сброс `Staticache`) → **перезапустить Lampa на ТВ** (`tv_restart.sh`) → `probe_ui.py`. ## 🔴 КАРТОЧКА: механика 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 — почему каталог «не работает» **Что хотел Alex (дословно):** > «Я предполагал что будет отображаться то что и раньше (список cub) но В НЕГО будет > сверху добавлено несколько секций в которых будут РЕАЛЬНЫЕ ЭЛЕМЕНТЫ — а не сгенерируют > пустые заглушки! Нахуя это надо то?! Чё блядь значит оно так и должно быть?!» **Что сделано вместо этого — две независимые ошибки:** > ⚠️ **ОПРОВЕРГНУТО ТЕСТОМ 2026-09-22 (поздний вечер).** Раздел ниже описывает > **неверную гипотезу** («`menu` подменяет / `main` добавляет»). Проверка на живом > сервере показала: **блока `main` в YAML не существует** — он ломает парсинг. > Подмену разделов делает **`plugin.js`**, а не YAML. Читать раздел > **«🔴 ПИТФОЛЛ YAML: `main` ЗАПРЕЩЁН»** ниже — там правильная механика. ### Ошибка 2 — источник данных: парсинг `.md` вместо Radarr API Генератор читал `movies-watchlist.md` — **текстовый список названий**. В нём **нет ни TMDB ID, ни постеров**. Результат: | Метрика | Значение | |---|---| | Всего позиций (films) | 360 | | Из них с постером (`img != null`) | **0** | | Из них с реальным ID (`film/...`) | **18** | Остальные 342 отдавались с `id` = **ссылка на поиск Кинопоиска** (`index.php?kp_query=...`). Отсюда жалоба «при открытии элемента пишет *здесь пусто*»: Lampa идёт по ссылке, получает страницу поиска, а не фильм. **Замер 2026-09-22 (доказательство, воспроизводимо):** ```bash curl -s "http://192.168.1.14:9118/catalog/list?plugin=watchlist&cat=films" \ | jq '[.results[] | select(.img == null)] | length' # → 360 curl -s "..." | jq '.results | length' # → 360 curl -s "..." | jq '[.results[] | select(.id | startswith("film/"))] | length' # → 18 ``` То есть **постеров 0 из 360**, рабочих карточек 18 из 360. **Почему «девять» с маленькой буквы и `Gods don't give gifts`** — это не баг каталога, а буквально строки файла (проверено `grep -n`): ``` 56: - [ ] Gods don't give gifts 64: - [ ] ❓ девять ``` Каталог отдаёт строки как есть. Обе — в секции `## Новинки` (отступ 2 пробела, подсписок). Alex: «Отображаются элементы типа "девять"(с маленькой буквы), Gods don't give gifts — который вообще ещё не вышел — короче не реальные фильмы а буквально пункты списка.» **Правильный источник — Radarr API.** Alex: «Они уже резолвятся же блядь!! Оно же добавлялось в arr!» — TMDB ID и постеры уже есть там, потому что фильмы добавлялись в Radarr. ```bash # API-ключ Radarr grep -oE "[^<]+" /srv/dev-disk-by-uuid-6194539b-.../docker/radarr/config.xml # → cbcb8ec320104359920c0f611d503c7b # ⚠️ ВАЖНО: -L обязателен! Без него Radarr отдаёт 307, тело пустое curl -sL "http://localhost:7878/api/v3/movie?apikey=$KEY" | jq length # → 50 ``` **Форма ответа Radarr (то, что нужно было брать с самого начала):** ```json { "tmdbId": 1292695, "title": "They Will Kill You", "year": 2026, "hasFile": true, "images": [{"coverType":"poster", "remoteUrl":"https://image.tmdb.org/t/p/original/6oI4oQKTWMVUlr8Ivqydp28R"}] } ``` Есть **всё**: TMDB ID → рабочий `href`, `remoteUrl` → постер, `title`/`year` → название, `hasFile` → скачан или нет. Плюс постеры локально: `/config/MediaCover/` (46 каталогов). ⚠️ **Покрытие:** в Radarr **50 фильмов**, в `.md` — **360**. Разница (310) — то, что в список записано, но в Radarr не заказано. Что с ними делать — **решение Alex не получено**, открытый вопрос №1 ниже. ### Что НЕ надо было делать (записано, чтобы не повторять) - Парсить текст вместо API, где данные уже резолвлены - Складывать `menu` + `main` одновременно - Считать «список названий» готовым каталогом для UI с постерами ### Alex: корневая претензия к процессу > «А нахуя ты это так реализовал?? Подумать блядь вообще не твое!» Разбор: задача звучала как «покажи список в UI» — и была прочитана буквально («отдать строки файла»). **Надо было сначала выяснить, откуда брать постеры и ID** (а ответ — Radarr, куда фильмы и добавлялись), а не начинать с парсера текста. ## Задача Показывать список фильмов Alex (`family/documents/movies-watchlist.md`) **в UI Lampa на ТВ** — каталогом. Не локальный плейбэк. Формулировка Alex: «отображение фильмов из списка». **Уточнение от 2026-09-22 (важное!):** каталог **добавляется СЕКЦИЯМИ СВЕРХУ** к родному каталогу Lampa. Родной CUB-контент **не подменяется и не исчезает**. Элементы — **реальные** фильмы с постерами и карточками, а не пустые заглушки (см. «🔴 АРХИТЕКТУРНАЯ ОШИБКА»). ⚠️ **Что НЕ спрашивать повторно.** Задача — **отображение**, не воспроизведение. Список — это перечень, а не источник видео. Вопрос «а что играть по клику?» — **выдумка сверх постановки**, Alex на него реагировал раздражением («я чё тебе сказал в постановке задачи»). Штатное воспроизведение Lampa (торренты/онлайн) работает само, отдельно решать нечего. ## ✅ Затык снят: чем это делается В образе Lampac уже есть модуль **`Catalog`** — YAML-каталоги → плагин Lampa `/catalog.js`. Сейчас **выключен**. Писать плагин с нуля **не нужно**. ## Как работает Catalog (разобрано по коду 2026-09-22) **Файлы в контейнере:** `/lampac/module/Catalog/` — `Controllers/`, `plugin.js`, `README.md`, `sites/*.yaml` (10 готовых примеров: `vokino`, `rezka`, `kinopub`, `anime`, …). ### Механика | Что | Как | |---|---| | Данные | `sites/.yaml` — описание сайта: `host`, `menu`, `search`, `list`, `content` | | Подстановки | `override/.yaml` и `override/_.yaml` — переопределения поверх базового | | Роуты | `/catalog.js` (плагин), `/catalog` (JSON каналов), `/catalog/list`, `/catalog/card` | | WAF | `^/catalog/` → **10 запросов/сек** | | Имя файла | только `[a-z0-9-]`; есть fallback-поиск по `displayname` | ### Ключевое: обязателен HTTP ```csharp if (!url.StartsWith("http")) url = $"{init.host}/{url}"; // ListController.cs:87 ``` `host` из YAML сканируется **по HTTP** (`Http.Get` / Playwright / RCH). **Локальные файлы напрямую Catalog читать не умеет** — только HTTP-ответ. ### Два режима парсинга ```csharp bool? jsonPath = contentParse.jsonPath ?? init.jsonPath; if (jsonPath == true) json = JToken.Parse(html); // JSON-режим else doc.LoadHtml(html); // HTML-режим ``` - `jsonPath: true` → ответ парсится как **JSON**, элементы берутся по `nodes` (`$.channels[*].details`) - иначе → HTML через HtmlAgilityPack 🔴 **Это и есть рычаг:** любой **свой HTTP-эндпоинт, отдающий JSON нужной формы**, подойдёт как `host`. Тогда задача сводится к генерации JSON + YAML-обёртки, без правки кода Lampac. ### Форма элемента (`content` в YAML) ```yaml content: nodes: "$.channels[*].details" # jsonPath-выражение (JSON) или CSS/XPath (HTML) name: { attribute: name } original_name:{ attribute: originalname } href: { attribute: ident } image: { attribute: poster } year: { attribute: released } serial_key: { attribute: type, format: | return value is "serial" ? value : null; } args: - { name_arg: vote_average, attribute: rating_kp } ``` Карточка — отдельная секция `card_parse` + `card_args` (описание, рейтинги, жанры, страна). Полный живой пример — `sites/vokino.yaml` (копия в [[personal/tech/lampac-deployment]], раздел Catalog). ## Включение модуля — 2 правки ```jsonc // current.conf (внутри контейнера /lampac/current.conf) "SkipModules": [ "Tracks", "Transcoding", ... ] // ← убрать "Catalog" (строка ~277) "plugins": { ..., "catalog": true } // ← было false (строка ~1653) ``` 🔴 **ПИТФОЛЛ — `init.conf` НЕ смонтирован.** В нашем `docker-compose.yml` нет volume на `/lampac/init.conf`, файл живёт в образе. Правки через `docker exec` **исчезнут при пересоздании контейнера** (`docker compose up`, watchtower-обновление образа). Варианты: - (а) добавить volume `./config/init.conf:/lampac/init.conf:ro` — надёжно, но нужен первичный `docker cp` - (б) править через **AdminPanel** `/adminpanel` (модуль, правит `init.conf` + `current.conf`) - (в) `docker cp` + commit — плохо **Рекомендация: (а)** — конфиг на HDD, переживает пересоздание. ## Архитектура решения ``` family/documents/movies-watchlist.md ← источник (vault, git-синхрон) │ существующий парсер: watchlist-sync/parser.py ▼ генератор (Python, на Кра́кене) │ пишет JSON нужной формы ▼ /lampac/... JSON-эндпоинт, доступный по HTTP с Lampac │ ▼ sites/watchlist.yaml ← Catalog-описание, host=<наш JSON URL> menu: Фильмы / Мультфильмы / Аниме (по секциям .md) │ ▼ Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js ``` **Откуда отдавать JSON — варианты (выбрать на этапе реализации):** | # | Где | Плюс | Минус | |---|---|---|---| | 1 | **статика в `/lampac/wwwroot/`** | ничего не поднимать, Lampac сам отдаёт | только чтение, обновление = перезапись файла | | 2 | **отдельный лёгкий HTTP-сервер** на Кра́кене | есть API/логика | ещё один контейнер | | 3 | `file://` | — | ❌ не поддерживается (только http) | **Склоняюсь к (1)** — генерировать `wwwroot/watchlist.json`, Lampac отдаёт его сам, без новых сущностей. ## Обновление каталога — реализовано Скрипт `~/scripts/watchlist-catalog.sh` (root:root, ставить только `sudo install -m 755`): 1. `python3 $LAMPAC/scripts/gen_watchlist.py $LAMPAC/wwwroot/watchlist.json` 2. `docker restart lampac` — **обязательно**, сбрасывает кэш Catalog (см. питфолл 3) Cron: `30 2 * * *` → лог `~/logs/watchlist-catalog.log`. Время выбрано после `watchlist-nightly` (01:00) и `watchlist-cleanup` (02:00). Бэкап crontab перед установкой: `~/crontab-backup-20260922-112915.txt`. ⚠️ **`watchlist-catalog.sh` сделан отдельной задачей, а не врезан в `watchlist-nightly.sh`.** Причина: `nightly` — рабочий боевой скрипт, его падение остановило бы весь пайплайн списка. Отдельный cron падает изолированно. ## Что осталось — ПЕРЕДЕЛКА (после признания архитектуры неверной) | # | Шаг | Статус | |---|---|---| | 1 | Генератор берёт данные **из Radarr API** (`localhost:7878/api/v3/movie`), а не парсит текст | ✅ сделано (9 из Radarr) | | 2 | Подставлять в `id` → `tmdbId`, в `img` → `images[].remoteUrl` (постер), `year` → `year` | ⏳ | | 3 | ~~Из `sites/watchlist.yaml` убрать блок `menu`, оставить `main`~~ ❌ **НЕВОЗМОЖНО** — `main` в YAML ломает парсинг, `menu` обязателен. Подмену делает `plugin.js`. См. «🔴 ПИТФОЛЛ YAML» | ❌ опровергнуто | | 4 | Постеры кэшировать локально (Radarr `MediaCover/` или проксировать через `tmdbproxy`) | ⏳ | | 5 | Фикс `plugin.js` (`enumerable:true`) — **отдельная независимая правка**, см. ниже | ⏳ ждёт апрува | **Порядок:** шаги 1–4 закрывают жалобу «пустые заглушки + исчез CUB». Шаг 5 — про то, что источника вообще не видно в списке выбора. Приёмка (формулировка Alex): **родной список CUB на месте, сверху добавлены ряды с реальными фильмами из списка, у элементов есть постеры и рабочие карточки.** ## Диагностический скрипт для ТВ (`/tmp/lampa_probe.py`) Правки в UI Lampa по SSH напрямую не сделать, но **состояние читается** через DevTools: ```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).join('|') # Object.getOwnPropertyDescriptor(Lampa.Api.sources,'Мой список') # Lampa.Api.sources['Мой список'].getCatalog() ``` Требует `pip3 install websocket-client` (стоит). См. [[family/how-to/tv-lampac-plugin]]. ## Известные ограничения | Ограничение | Детали | |---|---| | **Постеров нет** | `image` пустой. Опционально — тянуть через TMDB-кэш Lampac (`tmdbproxy`) | | **Карточки нет** | Только список названий. `card_parse` не настроен — Lampa может показывать пустую карточку | | **`🗓️` upcoming показываются** | Не прячем. Решение Alex не запрашивалось | | **Перезапуск контейнера** | Каждое обновление каталога = `docker restart lampac` (~30 сек простоя). Кэш не инвалидируется иначе | ## ⚠️ Что проверено, а что нет **Проверено по HTTP (серверная часть):** | Проверка | Результат | |---|---| | `/catalog.js` по IP | 200, ~32 КБ, `api_host:'http://192.168.1.14:9118'` | | Каталог в `/catalog` | **7 ключей**, «Мой список» первым (Rezka, Kinobase, FilmixApp, VoKino, Eneyida, KinoUkr, Мой список) | | JSON-файлы | `watchlist.json` + `watchlist-{films,new,cartoons,anime}.json` — все 200 | | Выдача по категориям | films 360 · new **33** · cartoons 3 · anime 7 (после фикса сирот 2026-09-22) | | Формат ответа `/catalog/list` | `{page, results, total_pages}`, `total_pages: 1` для films | | Сортировка | ⭐ → рейтинг ↓ → ужасы | | Чистота названий | эмодзи-маркеров 0 | | Сирота «Конец света» | ❌ отсутствует во всех 4 категориях | **НЕ проверено:** - ✅ **UI Lampa на ТВ — ПРИЧИНА НАЙДЕНА 2026-09-22** (см. ниже «🔴 ГЛАВНЫЙ ПИТФОЛЛ»). Диагностика через Chrome DevTools ТВ (`http://192.168.1.70:9998/json`): - ТВ **физически достаёт** Кра́кен — `curl` с ТВ отдаёт `/catalog` 200 за 0.26 с. - Lampa **работает и ходит на сервер** — соединение `192.168.1.70 → 192.168.1.14:9118` ESTABLISHED. - `Storage.field('source')` = **`'Мой список'`** — источник уже выбран! - `Lampa.Api.sources['Мой список']` **существует** и отдаёт каталог (`getCatalog()` → `search, search_lazy, movie, cartoons, anime, menu, main`). - ❌ НО: `Object.keys(Lampa.Api.sources)` = `'tmdb|cub'` — **источника нет в перечислении**. - Причина: `defineProperty(..., {get: ..., enumerable:false, configurable:false})`. - **Диагноз: баг в `plugin.js` самого Lampac, а не в нашей настройке.** UI Lampa строит список источников через `Object.keys()` — «Мой список» из него выпадает, поэтому в UI источник не видно и выбрать его нельзя. - ❌ Боевой прогон крона в 02:30 (скрипт отработал вручную успешно). - ❌ Карточка/постеры в UI. ## 🔴 ГЛАВНЫЙ ПИТФОЛЛ: источник не виден в UI Lampa (`enumerable: false`) **Симптом.** Alex: «Ни на главной ни в Фильмах нет! Там только Сейчас смотрят, рекомендуем итд». Плагин добавлен, серверная часть отдаёт всё корректно, а в UI источника нет. **Диагностика (по возрастанию точности):** 1. `ssh kraken 'sudo ss -tn | grep ":9118"'` — сначала показал **0 соединений** (ложный вывод: замер сделан в момент старта Lampa). Через 30 с на ТВ: `netstat -tn | grep 9118` → **1 ESTABLISHED + 6 TIME_WAIT** — Lampa скачала все файлы. 2. Foreground-приложение: `luna-send -n 1 -f luna://com.webos.applicationManager/getForegroundAppInfo {}` → `com.lampac.tv`. Запускать надо `launch` с **экранированным JSON в кавычках**, иначе `errorCode: 2 invalid parameters`. 3. Доступ к отладчику ТВ: **`curl http://192.168.1.70:9998/json`** — webOS отдаёт Chrome DevTools Protocol на порту 9998 (у `WebAppMgr` в аргументах `--remote-debugging-port=9998`). Через `websocket` + `Runtime.evaluate` читаются переменные живой Lampa. 4. Ключевой замер: `JSON.stringify(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; } }); ``` Свойство создаётся не-enumerable → `Object.keys()` его не видит → Lampa не рисует источник в панели выбора. При этом `Api.sources['Мой список']` доступен напрямую и каталог отдаёт. **Фикс** (одна правка, два вхождения): ```js Object.defineProperty(Lampa.Api.sources, key, { get: function(){ return Api; }, enumerable: true, configurable: true }); ``` **Как применять — `plugin.js` живёт в образе, не на HDD!** (проверено: `docker inspect` не показывает монтирования на `module/Catalog`, монтируются только `catalog/sites` и `catalog/override`). Правка через `docker exec` **исчезнет при обновлении образа**. Предложенный путь (на апруве у Alex, 2026-09-22): ```yaml # docker-compose.yml volumes: - ./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro ``` ```bash # 1. забрать, поправить локально (sed/grep по файлу не применять — только patch) ssh kraken 'docker exec lampac cat /lampac/module/Catalog/plugin.js' > /tmp/catalog-plugin.js # 2. бэкап cp /tmp/catalog-plugin.js /tmp/catalog-plugin.js.bak-$(date +%Y%m%d-%H%M%S) # 3. правка двух вхождений defineProperty → +enumerable:true,configurable:true # 4. проверка синтаксиса ПЕРЕД заливкой node --check /tmp/catalog-plugin.js # 5. на HDD, добавить volume, пересоздать scp /tmp/catalog-plugin.js kraken:/srv/.../lampac/catalog/plugin.js ssh kraken 'cd /srv/.../lampac && docker compose up -d' # 6. проверка на ТВ: Object.keys(Lampa.Api.sources) должен стать 'tmdb|cub|Мой список' ``` **Откат:** убрать строку volume из compose → `docker compose up -d`. Плюс бэкап файла в `/tmp`. **Статус: правка НЕ применена** — ждёт апрува Alex (2026-09-22). ## Готовые команды диагностики ТВ (переиспользовать) ```bash # 1. Что запущено на ТВ (luna-send пишет в tty → нужен script -q -c) ssh root@192.168.1.70 'script -q -c "luna-send -n 1 -f \ luna://com.webos.applicationManager/getForegroundAppInfo {}" /dev/null' # 2. Запустить приложение (JSON экранирован И обёрнут в кавычки) ssh root@192.168.1.70 'script -q -c "luna-send -n 1 -f \ luna://com.webos.applicationManager/launch \"{\\\"id\\\":\\\"com.lampac.tv\\\"}\"" /dev/null' # 3. Достаёт ли ТВ Кра́кен ssh root@192.168.1.70 'curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \ --max-time 8 http://192.168.1.14:9118/catalog' # 4. Живые соединения с Lampac (на ТВ, не на сервере!) ssh root@192.168.1.70 'netstat -tn 2>/dev/null | grep 9118' # 5. DevTools-цель Lampa на ТВ curl -s --max-time 8 "http://192.168.1.70:9998/json" | /usr/bin/jq -r '.[] | "\(.type): \(.url)"' ``` Скрипты-пробы DevTools: `/tmp/lampa_probe.py` (состояние источника), `/tmp/lampa_probe3.py` (асинхронный вызов `menu()` каталога). ⚠️ `tcpdump` на Кра́кене **нет** — для съёма трафика ставить не стали, использован DevTools-подход (дал больше: видно состояние JS, а не только пакеты). ⚠️ `luna://com.webos.service.capture/takeScreenShot` на этом ТВ **не существует** (`Unknown method`) — скриншот так не снять. ## Файлы реализации (на HDD Кра́кена) Корень: `/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac/` | Путь | Роль | |---|---| | `config-module/init.conf` | включает Catalog → `/lampac/init.conf:ro` | | `catalog/sites/watchlist.yaml` | описание каталога | | `catalog/override/` | переопределения (пусто) | | `wwwroot/watchlist.json` | все секции | | `wwwroot/watchlist-.json` | по файлу на категорию | | `scripts/gen_watchlist.py` | генератор | | `docker-compose.yml` | 7 монтирований; бэкапы `.bak-*` | На хосте: `~/scripts/watchlist-catalog.sh`, лог `~/logs/watchlist-catalog.log`. ## Откат ```bash # выключить каталог ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \ /srv/.../lampac/config-module/init.conf && docker restart lampac' # в Lampa убрать плагин catalog.js # cron: crontab -l | grep -v watchlist-catalog | crontab - ``` Ничего в коде Lampac не патчилось — только конфиг и данные. ## Статус: ✅ РЕАЛИЗОВАНО 2026-09-22 Каталог «Мой список» работает, отдаётся в Lampa через `/catalog.js`. ### Что сделано | # | Шаг | Результат | |---|---|---| | 1 | Включён модуль Catalog | `SkipModules` без Catalog + `catalog: true` → `/catalog.js` 200 | | 2 | `init.conf` смонтирован | `./config-module/init.conf:/lampac/init.conf:ro` — правится на HDD, не теряется | | 3 | `sites/` + `override/` смонтированы | `./catalog/sites`, `./catalog/override` | | 4 | `wwwroot` смонтирован | `./wwwroot` — Lampac сам отдаёт JSON по HTTP | | 5 | Генератор написан | `lampac/scripts/gen_watchlist.py` | | 6 | `sites/watchlist.yaml` создан | `lampac/catalog/sites/watchlist.yaml` | | 7 | Автообновление в cron | `30 2 * * *` → `~/scripts/watchlist-catalog.sh` | | 8 | Проверка | 4 категории: films 360, new 34, cartoons 3, anime 7 | ### Архитектура (финальная) ``` family/documents/movies-watchlist.md ← источник │ gen_watchlist.py (свой парсер, ~200 строк) ▼ lampac/wwwroot/watchlist.json ← все секции lampac/wwwroot/watchlist-.json ← по файлу на категорию │ Lampac отдаёт wwwroot по HTTP сам ▼ lampac/catalog/sites/watchlist.yaml ← описание для Catalog │ plugin=watchlist, jsonPath: true, nodes: $.items[*] ▼ Lampa на ТВ: плагин http://192.168.1.14:9118/catalog.js ``` ### 🔴 Питфоллы, найденные по ходу (не повторять) 1. **Catalog ВЫБРАСЫВАЕТ элемент с пустым `href`.** `ListController.cs`: `if (!IsNullOrWhiteSpace(name) && !IsNullOrWhiteSpace(href))`. Симптом: из 404 позиций отдалось 52 — ровно те, у кого есть ссылка Кинопоиска. **Фикс:** генератор подставляет fallback-href `https://www.kinopoisk.ru/index.php?kp_query=<название>`. 2. **Catalog НЕ подставляет `{cat}` в `nodes`.** JSONPath статичен, отфильтровать секцию внутри одного файла нечем. Симптом: все категории отдавали весь список (404 вместо 360/34/3/7). **Фикс:** генератор пишет отдельный плоский файл на категорию (`watchlist-films.json` и т.д.), `nodes: "$.items[*]"` одинаков для всех. 3. **Кэш Catalog без версии данных.** Ключ `catalog:{plugin}:{search}:{sort}:{cat}:{page}` — не зависит от содержимого. Симптом: после перегенерации JSON выдача не менялась. **Фикс:** `docker restart lampac` в скрипте обновления. 4. **`init.conf` не был смонтирован.** Файл живёт в образе; правки через `docker exec` теряются при пересоздании. Правки шли в `current.conf` (сгенерированный) — неверная цель. **Фикс:** volume `:ro` + свой минимальный `init.conf` (перекрывает `base.conf`). 5. **`api_host` подставляется из запроса** (`{localhost}` → хост, по которому пришли). По IP `192.168.1.14` отдаёт правильный адрес — специально ничего настраивать не нужно. 6. **Кириллица в inline-jq внутри `ssh '...'`** ломает команду (tirith + quoting). Выносить в файл или убирать строки с кириллицей. 7. **Осиротевшие строки в `## Новинки` давали дубли в каталоге.** Строки без `👍`/`👎` (например `- [ ] ⬇️ Название`) `process-thumbs` не трогал — они висели в Новинках навсегда. Если фильм уже `[x]` в главном списке — получался дубль: непросмотренный «Конец света» при просмотренном оригинале. **Фикс в `watchlist-sync/parser.py`** (удаление сирот + флаг `_patch_dropped`) и `cli.py`. Разбор: [[family/projects/watchlist-automation]], секция «ОСИРОТЕВШИЕ СТРОКИ». 8. **Двойное экранирование при правке `parser.py` скриптами.** `r'\1'` превращался в байт `\x01`, `r'\b'` — в `\x08`. Синтаксис валиден, регекспы молчат. **Проверять файл на `chr(1)` и `chr(8)` после патча.** ### Управление ```bash ssh kraken cd /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/lampac # перегенерировать каталог + сбросить кэш bash ~/scripts/watchlist-catalog.sh # логи tail -f ~/logs/watchlist-catalog.log # только генератор (без перезапуска) python3 scripts/gen_watchlist.py \ /home/kraken/obsidian/family/documents/movies-watchlist.md \ wwwroot/watchlist.json ``` ### Откат ```bash # выключить каталог ssh kraken 'sed -i "s/\"catalog\": true/\"catalog\": false/" \ /srv/.../lampac/config-module/init.conf && docker restart lampac' # в Lampa убрать плагин catalog.js ``` ## Правила отбора и сортировки (утверждено Alex 2026-09-22) ### Фильтрация | Правило | Действие | |---|---| | `[ ]` непросмотренные | **показывать** | | `[x]` просмотренные | ❌ скрыть | | `👎` | ❌ скрыть | | `🗓️` upcoming | показывать (решить при реализации — есть в файле) | | `❓` ambiguous | показывать (название как есть) | ### Сортировка внутри списка — трёхключевая 1. **`⭐` (U+2B50) — первыми** 2. **Рейтинг — по убыванию** (IMDb/KP из строки файла; строки `[название](kp) (год) IMDb N.N KP N.N`) 3. **Ужасы (`🧟♀️`, TMDB genre_id=27) — последними** ⚠️ **Только `⭐` (U+2B50)** — как в `sync-down` (см. [[family/projects/watchlist-automation]]). `✭` (U+272D) приоритетом **не является**. ### Порядок секций 1. **Главный список — первым.** Это `## Фильмы` 2. Дальше — **остальные по порядку в файле**: `## Новинки - 👍/👎`, `## Мультфильмы`, `## Аниме`, … ⚠️ `## Новинки👎` — **не показывать** (это скрытая секция отказов). ⚠️ Секция `## Новинки` содержит `👍`/`👎`; `👎`-строки прячем, `👍` показываем. ### Источник рейтинга В строках со ссылками Кинопоиска рейтинг **уже в тексте**: `IMDb 7.6 KP 8.2`. Для строк без рейтинга (например `- [ ] 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)}' ``` ## 🟢 РЕШЕНИЕ: первая категория игнорирует `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` → «Мой список» **полностью исчез** из `/catalog` (остались только Eneyida, FilmixApp, KinoUkr, Kinobase, Rezka, VoKino). Запрос `catalog/list?plugin=watchlist` отдавал **пустоту**. В логах Lampac — **тишина**, ошибка не пишется, сайт просто отбрасывается. **Причина:** `CatalogSettings` **не имеет поля `main`** для входного YAML. YamlDotNet давится неизвестным полем → `ModInit.goInit(site)` возвращает **null** → `ApiController.cs:70` отбрасывает сайт: ```csharp if (init == null || !init.enable || init.menu == null || init.hide) continue; ``` ⚠️ Проверено обе формы — **обе ломают**: ```yaml main: # список — НЕ работает - categories: Фильмы: films ``` ```yaml main: # словарь — НЕ работает Фильмы: http://... ``` **Как на самом деле:** Catalog **сам генерирует `main`** в JSON-ответе `/catalog`, выводя его из блока `menu`. Проверено — в ответе есть **и `menu`, и `main`** с одинаковым содержимым: ```json "menu": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... }, "main": { "Фильмы": "/catalog/list?plugin=watchlist&cat=films", ... } ``` **Писать `main` в YAML не нужно и нельзя.** Достаточно `menu`. ### Правильная форма YAML (проверенная, рабочая) ```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 } ``` ### Сопутствующие питфоллы (все проверены на живом сервере) 1. 🔴 **`menu` ОБЯЗАТЕЛЕН.** Не «подменяет разделы», а **регистрирует канал**. `ApiController.cs:70`: без `menu` сайт отбрасывается. Убирать нельзя. 2. 🔴 **`host` + `uri` СКЛЕИВАЮТСЯ.** `ListController.cs:53`: ```csharp string url = $"{init.host}/{(page==1 && init.list?.firstpage != null ? init.list?.firstpage : init.list?.uri)}"; ``` Значит `uri`/`firstpage` должны быть **относительными** (`watchlist-films.json`). Полный URL даёт двойной префикс → пусто: `http://127.0.0.1:9118/http://127.0.0.1:9118/watchlist-films.json` 3. 🔴 **Явно указанный `format` в `menu` вызывает перезагрузку страницы при каждом выборе категории.** Если не нужен — убрать `format` из `menu`, оставить только `categories`. (Наблюдение Alex: «перезагружает прогрузку страницы при каждом выборе раздела».) ### Кто на самом деле подменял родные разделы Lampa **НЕ YAML.** Это `plugin.js` (он же `/catalog.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; ... Lampa.Router.call('category', {...}); e.abort(); // ← перехват клика по родному разделу } }); ``` **Перехват срабатывает ТОЛЬКО когда выбран источник «Мой список».** Родной CUB/tmdb не трогается, пока источник не переключён на наш. ### Как реально добиться «CUB + мой список сверху» | Путь | Реализуемость | |---|---| | Блок `main` в YAML | ❌ **невозможно** — ломает парсинг | | Оставить `menu` | ⚠️ источник работает, но разделы подменяются, пока он выбран | | Патч `plugin.js` | ⚠️ возможно: убрать `Listener.follow('menu')` — перехвата не будет, но и секции сверху не появятся | **Открытый вопрос для Alex:** родной CUB и «Мой список» — это **два источника Lampa**, переключаются в «остальное → основной источник». Одновременно показывать нельзя без патча `plugin.js`. Решение за Alex. ## ✅ РЕШЕНИЕ НАЙДЕНО 2026-09-22 — полная архитектура (проверено) ### Что уже есть в системе (нашёл, проверено) **Модуль `watchlist_sync/resolver.py`** (`/srv/.../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`** — клиент Radarr/Sonarr. 🔴 **ВОТ ИСТОЧНИК СКАЧКИ:** ```python "monitored": True, "addOptions": {"searchForMovie": True}, # ← качает сразу после добавления ``` Для сериалов: `"addOptions": {"searchForMissingEpisodes": True}` — то же самое. ### Ключевая связка ID | База | Где есть | Формат | |---|---|---| | **Кинопоиск (kp)** | в `.md`: 68 строк со ссылками `kinopoisk.ru/film//` | `11825624` | | **TMDB** | в Radarr (`tmdbId`), в resolver (`Candidate.tmdb_id`) | `1292695` | | **IMDb** | в Radarr (`imdbId`), в `.md` только рейтинг | `tt31728330` | ⚠️ **Прямой связи kp↔tmdb в файле нет.** Связка делается через **`resolver.py`**: он по названию находит и `kp_id`, и `tmdb_id` одновременно. Это его основная работа. ### Замер Radarr (после шагов 1–3, 2026-09-22) | Метрика | Значение | |---|---| | Фильмов | **50** | | `monitored` | **0** ← автоскачка выключена | | `rssSyncInterval` | **0** ← в RSS не ходит | | Очередь | **0** | | С `tmdbId` | 50 (все) | | С постером | **46** | | С рейтингом TMDB | 50 (все) | | С файлом на диске | 24 (не трогали) | Sonarr: 13 сериалов, `monitored` 0, RSS 0, очередь 0. ### Поля Radarr для каталога (проверено, `GET /api/v3/movie`) ```json { "tmdbId": 1292695, "imdbId": "tt31728330", "title": "They Will Kill You", "originalTitle": "They Will Kill You", "year": 2026, "titleSlug": "1292695", "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`) — уже включён. ### КАК ИГРАЕТ LAMPA (проверено — на диск НЕ пишется) | Компонент | Значение | Смысл | |---|---|---| | TorrServer | `/ts/echo` → `MatriX.135` | жив, встроен в Lampac | | **`UseDisk`** | **`false`** | 🔴 **на диск НЕ пишет** | | `CacheSize` | 100663296 (96 МБ) | кэш в RAM | | `TorrentsSavePath` | `""` (пусто) | пути сохранения нет | | `TorrentDisconnectTimeout` | 180 | раздача живёт 3 мин после стопа | | Занятые торренты | `[]` (пусто) | ничего не висит | **Поиск торрентов:** Lampa → `jackett_url = 192.168.1.14:9118` → **Lampac сам отдаёт Jackett-совместимый API**: ```bash 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 **не нужен** (его нет в системе). 🔴 **`parser_torrent_type = 'jackett'`** в `lampainit.js` — это НЕ отдельный Jackett, а эндпоинт внутри Lampac. Так и задумано производителем. ### Итоговая архитектура (то, что нужно поставить) ``` family/documents/movies-watchlist.md ← источник (360 фильмов) │ ├─► resolver.py ──► TMDB + Кинопоиск │ │ tmdb_id + постер + рейтинг + жанры │ ▼ │ wwwroot/watchlist-.json ← каталог ДЛЯ LAMPA (с постерами!) │ │ │ ▼ │ sites/watchlist.yaml (main: только секции СВЕРХУ, БЕЗ menu) │ │ │ ▼ │ Lampa: плагин catalog.js → секции с постерами СВЕРХУ родного CUB │ └─► arr.py (searchForMovie: False) ──► Radarr/Sonarr └─► метаданные в базу БЕЗ скачки Просмотр: Lampa → TorrServer (RAM, UseDisk:false) → на HDD ничего ``` **Разделение ролей:** - **resolver.py** — постеры/ID для **каталога** (360 фильмов, включая те, что не в Radarr) - **Radarr** — справочник того, что уже добавлено (50), источник `hasFile` - **TorrServer** — воспроизведение, на диск не пишет ### Три правки (на апруве) | # | Файл | Правка | Зачем | |---|---|---|---| | 1 | `watchlist_sync/arr.py` | `searchForMovie: True` → **`False`** (+ у дочки то же для сериалов) | добавление без скачки | | 2 | ~~`catalog/sites/watchlist.yaml` — убрать блок `menu`, оставить `main`~~ ❌ **НЕВОЗМОЖНО** (опровергнуто тестом). Вместо этого — выбрать путь из трёх: оставить как есть / патч `plugin.js` / свой плагин. Решение Alex | ❌ отменено | | 3 | генератор каталога | брать постер/ID через `resolver` | реальные элементы вместо заглушек | ⚠️ Правка №1 **может уже не понадобиться**: шаги 1–3 (unmonitor + RSS 0) закрыли скачку на уровне состояния. Но `searchForMovie: True` в коде **включит её снова** при следующем добавлении из списка. Поэтому фикс нужен — иначе вернётся. ## 🎯 ПЕРЕДЕЛКА 2 — Radarr как источник метаданных без автоскачки (план Alex, 2026-09-22) ### Логика Alex (дословно) > «Мы блядь убираем автоскачку на диск так?! При этом ты оставляешь radarr/sonarr > источником, так?? И в него из списка добавляется скриптом, но не грузит автоматом так? > Сейчас ты отменяешь текущие загрузки, так??» **Расшифровка:** | # | Что | Как | |---|---|---| | 1 | **Автоскачка на HDD — выключить** | Radarr/Sonarr ничего не качают сами | | 2 | **Radarr/Sonarr остаются источником** | TMDB ID + постеры для каталога Lampa | | 3 | **Скрипт добавляет в них из списка** | `.md` → Radarr API, фильм получает метаданные | | 4 | **Но не грузит автоматом** | Добавление ≠ загрузка | | 5 | **Текущие загрузки — отменить** | 6 записей в очереди | ### Как это в Radarr технически (проверено 2026-09-22) ⚠️ **Важно для понимания:** в Radarr «добавить, но не качать» = **`monitored: false`**. Отдельного состояния «источник, но не грузить» нет. При добавлении через API Radarr **сам ходит в TMDB** и тянет метаданные (постер, год, описание) — даже при `monitored: false`. Это и есть решение задачи №2+№3+№4. ### Доступ к Radarr API (проверено 2026-09-22, воспроизводимо) ```bash # API-ключ лежит в config.xml (не в БД) ssh kraken 'grep -oE "[^<]+" \ /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker/radarr/config/config.xml' \ | sed 's/<[^>]*>//g' # → cbcb8ec320104359920c0f611d503c7b ``` 🔴 **ПИТФОЛЛ: `-L` обязателен.** Без него Radarr отдаёт **307 с пустым телом** и `jq length` падает: ```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 # → 200, 50 ``` **Конфиг Radarr (замер):** ```bash curl -sL ".../api/v3/config/indexer?apikey=$KEY" | jq .rssSyncInterval # → 30 (мин) ``` `rssSyncInterval: 30` = Radarr **каждые 30 минут** сам ходит в RSS-фиды и качает новое. Это вторая причина автоскачки помимо `monitored: true`. **Выключать надо оба.** ### Окружение Radarr на Кра́кене (для будущих сессий) | Параметр | Значение | |---|---| | Порт | `7878` (`0.0.0.0`) | | Контейнер | `radarr`, образ `lscr.io/linuxserver/radarr:latest` | | Конфиг на HDD | `/srv/...6194539b.../docker/radarr/config/` | | БД | `radarr.db` (+ `-shm`, `-wal`) | | Постеры | `/config/MediaCover/` — **46 каталогов** | | `sqlite3` в контейнере | ❌ **нет** (на хосте Кра́кена — **есть**, `/usr/bin/sqlite3`) | | Соседи | `sonarr:8989`, `prowlarr:9696`, `transmission` | ⚠️ В контейнерах `sqlite3` отсутствует (см. профиль Alex) — БД читать через `strings` или вытаскивать файл наружу и смотреть хостовым `sqlite3`. ### Текущее состояние Radarr (замер 2026-09-22) | Метрика | Значение | |---|---| | Фильмов всего | 50 | | `monitored: true` | **45** ← активны, ищут и качают | | `hasFile: true` | 24 (уже на диске) | | `rssSyncInterval` | **30** мин ← Radarr ходит в RSS каждые полчаса | | Очередь | **6 записей** (все `warning`, ничего не качается активно) | | Transmission | простаивает (1% CPU) | Очередь (id для отмены): ``` 598407147 Brokeback Mountain.mkv 1092005446 Гладиатор (2000) (DTS X) DV.mkv 914899324 Sorry.to.Bother.You.BDRemux.1080p.NNMClub.pk.mkv 1671545331 Мортал Комбат 2 (DTS-HD MA).mkv 1486734395 Gorod.boga.2002.BDRemux.1080p.mkv 139445264 День хомячка (2003) ``` ## ✅ ПРОВЕРКА «как это реализуемо» — полный аудит 2026-09-22 (вечер) Alex: «Мне надо чтоб lampa источники видела и играла фильмы из списка и cub. При этом мы перестаем качать торренты на диск! Как это реализуемо?? Проверь блядь все чтобы ты опять не обосрался». Проверено всё, что может качать или мешать. **Ничего не менялось — только чтение.** ### 🎯 Главная находка: TorrServer на диск НЕ пишет Ключевой факт, снимающий главный вопрос Alex («при этом фильмы не будут скачиваться на hdd я правильно понимаю?!»). Конфиг TorrServer внутри Lampac: ```bash ssh kraken 'docker exec lampac cat /lampac/data/ts/settings.json' | jq . ``` ```jsonc { "BitTorr": { "CacheSize": 100663296, // 96 МБ — RAM-кэш "UseDisk": false, // 🔴 НА ДИСК НЕ ПИШЕТ "TorrentsSavePath": "", // путь сохранения пуст "TorrentDisconnectTimeout": 180, // раздача живёт 3 мин после остановки "PreloadCache": 0, "ReaderReadAHead": 83 } } ``` **`UseDisk: false`** — TorrServer держит всё в RAM (96 МБ) и **на диск не пишет ничего**. Это ровно то, что нужно Alex. Настраивать не надо, уже так. ### TorrServer жив и работает (проверено) ```bash curl -s "http://192.168.1.14:9118/ts/echo" # → MatriX.135 (версия, живой) curl -s "http://192.168.1.14:9118/ts/torrents" # → 404 (это не тот эндпоинт) curl -s -X POST "http://192.168.1.14:9118/ts/torrents" \ -H 'Content-Type: application/json' -d '{"action":"list"}' # → [] пусто, ничего не висит ``` Кэш Lampac: `/lampac/cache/` = 31 МБ (SQLite, Playwright, gstreamer-registry) — **не фильмы**. `/lampac/data/` = 44 МБ (ts/settings.json, dlna/thumbs). Диск не забит. ### Поиск торрентов — Jackett внутри Lampac, отдельный НЕ нужен 🔴 **ПИТФОЛЛ-ЛОВУШКА: в `lampainit.js` стоит `parser_torrent_type = 'jackett'`, но отдельного контейнера Jackett НЕТ.** Это не ошибка — Lampac **сам отдаёт Jackett-совместимый API**. ```bash # что прописывает Lampac клиенту: curl -s "http://192.168.1.14:9118/lampainit.js" | grep -oE "Lampa.Storage.set\('[^']+', ?'[^']*'\)" # Lampa.Storage.set('jackett_url', '192.168.1.14:9118') # Lampa.Storage.set('jackett_key', '1') # Lampa.Storage.set('parser_torrent_type', 'jackett') # Lampa.Storage.set('parser_use', 'true') ``` `jackett_url` указывает **на сам Lampac**, а не на сторонний сервис. Проверка API: ```bash # ⚠️ работает БЕЗ префикса /jackett — на корне curl -s "http://192.168.1.14:9118/api/v2.0/indexers/all/results?apikey=1&Query=matrix" # → 200, реальные результаты: rutor, магнит-ссылки, Size, Seeders, ffprobe curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.14:9118/jackett/api/v2.0/..." # → 404 curl -s -o /dev/null -w "%{http_code}" "http://192.168.1.14:9118/api/v2.0/..." # → 200 ✅ ``` **Вывод:** поиск торрентов работает штатно, ничего поднимать не надо. ### Кто качает на диск — полный реестр (единственные виновники) ```bash ssh kraken 'docker ps --format "{{.Names}}\t{{.Image}}"' ``` | Сервис | Качает? | Замер | Нужен для задачи? | |---|---|---|---| | **Radarr** | ✅ да | 50 фильмов, **45 monitored**, очередь **6**, RSS 30 мин | ❌ мешает | | **Sonarr** | ✅ да | 13 сериалов, **13 monitored**, очередь **19** | ❌ мешает | | **Transmission** | ⚠️ исполнитель | **257** `.torrent`, CPU 0.76% (простаивает), NetIO 290G/26.7G | ⚠️ движок Radarr/Sonarr | | **TorrServer** | ❌ НЕТ | `UseDisk: false`, список торрентов `[]` | ✅ **вот чем играет Lampa** | Полный список контейнеров Кра́кена: `lampac · radarr · jellyfin · transmission · portainer · flaresolverr · prowlarr · sonarr · watchtower · rclone · xray-reverse-bridge · hermes-kraken · cloudflared` **Sonarr API** (для будущей работы, по аналогии с Radarr): ```bash # ключ: /srv/.../docker/sonarr/config/config.xml → 15fbec32f1cb4b05b1c2b229ce27359b curl -sL "http://localhost:8989/api/v3/series?apikey=$SONARR_KEY" | jq 'length' # → 13 curl -sL "http://localhost:8989/api/v3/queue?apikey=$SONARR_KEY" | jq '.totalRecords' # → 19 ``` ⚠️ Sonarr — **тоже источник автоскачки**, не только Radarr. Alex упомянул его не зря. Отключать надо оба, иначе Sonarr продолжит качать сериалы. ### Transmission — настройки (замер) ```bash ssh kraken 'docker exec transmission cat /config/settings.json' > /tmp/trans-settings.json ``` ```json {"download-dir": "/downloads", "rpc-port": 9091, "speed-limit-down-enabled": false, "peer-port": 51413, "alt-speed-enabled": false} ``` `/config/torrents/*.torrent` = **257 файлов** — это накопленный список раздач, **не признак активной качки** (CPU 0.76%). Файлы на диске не трогаем. ### Кто чем играет в Lampa (подтверждено) ```bash curl -s "http://192.168.1.14:9118/lampainit.js" | grep -iE "torrents_use|source" # lampa_settings.torrents_use = true ← кнопка торрентов включена # Lampa.Storage.set('source', 'cub') ← CUB — основной источник ✅ # Lampa.Storage.set('proxy_tmdb', 'true') ``` В `plugins_add` (серверные плагины Lampac, не список Lampa): `tmdbproxy.js · cubproxy.js · online.js · catalog.js · sisi.js · startpage.js · timecode.js · ts.js` — все `status: 1`. ### Итоговая схема (что получится после переделки) ``` ТВ: Lampa (com.lampac.tv) ├── источник CUB ── онлайн, работает как раньше ✅ └── источник «Мой список» ── секции СВЕРХУ, постеры из Radarr │ ▼ клик по фильму поиск торрента через Lampac (Jackett-API на :9118) │ ▼ TorrServer (UseDisk: false) → RAM-кэш 96 МБ → на HDD НЕ ПИШЕТ ✅ Radarr/Sonarr после переделки = ЧИСТЫЙ СПРАВОЧНИК МЕТАДАННЫХ (monitored: false + rssSyncInterval: 0) — не ищут, не качают ``` ### ✅ Порядок работ — ШАГИ 0–3 ВЫПОЛНЕНЫ 2026-09-22 | # | Действие | Сервис | Статус | |---|---|---|---| | 0 | Бэкап `radarr.db` + `config.xml` + `sonarr.db` + `config.xml` | оба | ✅ **сделано** | | 1 | Отменить очередь: 6 Radarr + 19 Sonarr | оба | ✅ **сделано** (оба 0) | | 2 | `monitored: false` — 45 Radarr + 13 Sonarr | оба | ✅ **сделано** (оба 0) | | 3 | `rssSyncInterval: 0` | оба | ✅ **сделано** (оба 0) | | 4 | Проверка: monitored 0, очередь 0, RSS 0, Transmission тих | оба | ✅ **сделано** | | 5 | Генератор каталога → постеры/ID через **TMDB + Radarr** (`gen_watchlist_v2.py`) | — | ✅ **написан, прогнан** (330/403 постеров) | | 6 | ~~В `sites/watchlist.yaml` убрать `menu`, оставить `main`~~ ❌ **ОТМЕНЕНО** — невозможно, см. «🔴 ПИТФОЛЛ YAML» | — | ❌ опровергнуто | | 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, остальные контейнеры. ### 📋 Как выполнены шаги 1–3 (скрипты и команды — воспроизводимо) **Шаг 0 — бэкапы (метка `20260922-123404`)** ```bash D=/srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker TS=$(date +%Y%m%d-%H%M%S) sudo cp $D/radarr/config/radarr.db $D/radarr/config/radarr.db.bak-$TS sudo cp $D/radarr/config/config.xml $D/radarr/config/config.xml.bak-$TS sudo cp $D/sonarr/config/sonarr.db $D/sonarr/config/sonarr.db.bak-$TS sudo cp $D/sonarr/config/config.xml $D/sonarr/config/config.xml.bak-$TS ``` Результат: `radarr.db` 4 374 528 Б, `sonarr.db` 4 870 144 Б + оба `config.xml` 650/648 Б. **Шаг 1 — очистка очереди.** Скрипт `/tmp/arr_clear_queue.py` (на Mac → `scp` → запуск на Кра́кене). `DELETE /api/v3/queue/?removeFromClient=true&blocklist=false`. Результат: Radarr 6 → **0**, Sonarr 19 → **0**. **Шаг 2 — снятие monitored.** - Radarr: `PUT /api/v3/movie/editor` с ключом **`movieIds`** → 45 → **0** - Sonarr: `PUT /api/v3/series/editor` с ключом **`seriesIds`** → 13 → **0** Скрипты: `/tmp/arr_disable_autograb.py` (Radarr, упал на Sonarr с HTTP 500), `/tmp/sonarr_unmonitor.py` (Sonarr, исправленный ключ). **Шаг 3 — `rssSyncInterval: 0`.** Скрипт `/tmp/radarr_rss_off.py`. Sonarr: 15 → 0 с первого раза. Radarr: **со второго** — первый заход вернул 30 (не отправлял `id`), фикс — отправлять **весь объект конфига с `id: 1`**. ### 📊 Состояние ПОСЛЕ выполнения (замер 2026-09-22) | Метрика | Radarr | Sonarr | |---|---|---| | Всего записей | 50 фильмов | 13 сериалов | | `monitored` | **0** (было 45) | **0** (было 13) | | `rssSyncInterval` | **0** (было 30) | **0** (было 15) | | Очередь | **0** (было 6) | **0** (было 19) | | С `tmdbId` | 50 (все) | — | | С постером | **46** | — | | С рейтингом TMDB | 50 (все) | — | | С файлом на диске | 24 (не трогали) | — | **Проверка пассивности:** Transmission за 45 секунд — NET `290GB/26.7GB` и BLOCK `214GB/393kB` **не сдвинулись ни на байт**. Логи Radarr (`grep search|download|grab`) за 2 минуты — **пусто**. TorrServer: список торрентов `[]`, `echo` → `MatriX.135`. ### 🔴 Питфоллы выполнения (найденные в этой сессии) 1. **`urllib` НЕ идёт по 307 автоматически, а `curl -sL` — идёт.** Симптом: все 25 DELETE в первом прогоне вернули `HTTP Error 307: Temporary Redirect`, **ничего не удалилось**, состояние не изменилось. **Фикс:** свой `HTTPRedirectHandler` подкласс, который для 307/308 сохраняет **метод и тело** (штатный `urllib` превращает DELETE в GET): ```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) ``` Тот же корень, что у `curl` без `-L` (питфолл в памяти) — только на стороне Python. 2. **Sonarr группирует эпизоды сезона под одним `id`.** Симптом: после удаления первой записи остальные возвращают `HTTP 404: Not Found`. Это **не сбой** — они исчезли вместе с родительской. Итог всё равно 0. 3. **Ключ поля в `*/editor` различается между Radarr и Sonarr.** Radarr `/movie/editor` → **`movieIds`**; Sonarr `/series/editor` → **`seriesIds`**. `ids` → HTTP 500 Internal Server Error (Sonarr). 4. **`PUT /api/v3/config/indexer` требует весь объект с `id`.** Частичный `{"rssSyncInterval": 0}` молча возвращает старое значение. Отправлять весь объект, полученный `GET`, с изменённым полем. 5. **Кириллица в inline-`jq` внутри `ssh '...'`** снова сломала команду (`unexpected INVALID_CHARACTER`) — 6 ошибок в одном вызове. Ключи объекта писать латиницей (`total`, `monitored`, `queue`), не `всего`/`очередь`. ### ❓ Открытый вопрос к Alex — ЗАКРЫТ Был вопрос «Radarr/Sonarr нужны как справочник или выключаются совсем?». **Alex ответил резко:** «Как ты блядь думаешь мне нужны блядь постеры или нет блядь?! Меня не ебет откуда оно их берет сделай чтобы работало как поставлено в задаче». → **Остаются как справочник метаданных.** Вопрос постеров больше не задавать. ### План работ (на апруве Alex) — ШАГИ 0–4 ВЫПОЛНЕНЫ **Шаг 0 — бэкап (обязательно, до всего)** ```bash ssh kraken 'cp /srv/.../docker/radarr/config/radarr.db /srv/.../radarr.db.bak-$(date +%Y%m%d-%H%M%S)' ssh kraken 'cp /srv/.../docker/radarr/config/config.xml /srv/.../config.xml.bak-$(date +%Y%m%d-%H%M%S)' ``` Причина: `radarr.db` — боевая база, содержит всю библиотеку. **Шаг 1 — отменить текущие загрузки (6 шт)** ```bash curl -sL -X DELETE "http://localhost:7878/api/v3/queue/?removeFromClient=true&blocklist=false&apikey=$KEY" ``` ⚠️ **Не** использовать `blocklist=true` — иначе релизы попадут в чёрный список и не найдутся при ручной загрузке. Файлы на диске **не трогаются**. **Шаг 2 — снять `monitored` со всех фильмов** ```bash curl -sL -X PUT "http://localhost:7878/api/v3/movie/editor?apikey=$KEY" \ -H "Content-Type: application/json" \ -d '{"movieIds":[<все 50>],"monitored":false}' ``` Результат: Radarr не ищет и не качает. Фильмы в базе с метаданными остаются. **Шаг 3 — выключить авто-поиск по RSS** ```bash curl -sL -X PUT "http://localhost:7878/api/v3/config/indexer?apikey=$KEY" \ -H "Content-Type: application/json" \ -d '{"rssSyncInterval":0,"id":1, ...остальные поля как есть}' ``` 0 = Radarr не ходит в RSS. Ничего не находит сам. **Шаг 4 — проверить** ```bash jq '[.[] | select(.monitored==true)] | length' # → 0 jq '.rssSyncInterval' # → 0 jq '.totalRecords' # очередь → 0 ``` **Что НЕ трогаем:** Prowlarr, Transmission, библиотеку на диске (24 файла), Sonarr (отдельная проверка ниже). ### ⚠️ Незакрытые вопросы к плану 1. **Sonarr** — Alex упомянул его вместе с Radarr. Проверить его настройки отдельно (сериалы в списке есть?). Это отдельная задача. **Вопрос задан Alex, ответа нет.** 2. **Ручная загрузка после отключения** — если Alex захочет скачать фильм вручную, нужно временно включить `monitored` или искать вручную через Interactive Search. Работает, но не автоматически. 3. **310 фильмов, которых нет в Radarr** — скрипт будет добавлять их с `monitored: false`, Radarr сам подтянет постеры из TMDB. Но часть названий найдётся криво («девять»). ### ✅ Подтверждено Alex 2026-09-22 (семантика задачи — не переспрашивать) Alex сам сформулировал схему и подтвердил её понимание ассистентом: > «При этом фильмы не будут скачиваться на hdd я правильно понимаю?! Играть будет > через torrserver так?» → ответ: **да, правильно**. **Разделение обязанностей (запомнить, чтобы не путать):** | Слой | Роль | Куда пишет | |---|---|---| | **Radarr/Sonarr + Transmission** | библиотека, автоскачка релизов | на HDD (библиотека) | | **Lampac TorrServer** | стриминг по требованию из UI Lampa | **ничего не сохраняет** — только временный кэш кусков | | **«Мой список» (Catalog)** | только показ секций с постерами | ничего не качает | То есть после выключения автоскачки в Radarr: **фильмы, которые Alex смотрит через Lampa, на HDD не попадают вообще** — TorrServer отдаёт поток и выбрасывает буфер. Файлы на диске остаются только у 24 уже скачанных (нужны Jellyfin). ⚠️ **`transmission` НЕ связан с Lampa.** Radarr качает через него релизы в свою библиотеку — это отдельный контур. Вопрос «через что играет TorrServer» и «что качает Radarr» — **разные вещи**, не смешивать в объяснениях. ### 🔴 Правка `plugin.js` (`enumerable:true`) — всё ещё НЕ применена Независимая от ПЕРЕДЕЛКИ 2 правка. Файл лежит локально `/tmp/catalog-plugin.js` (10738 байт, `node --check` чистый, 2 вхождения исправлены), бэкап `/tmp/catalog-plugin.js.bak-20260922-121452`. На Кра́кене и в образе — **оригинал**. Нужен volume `./catalog/plugin.js:/lampac/module/Catalog/plugin.js:ro` в compose. Подробности — раздел «🔴 ГЛАВНЫЙ ПИТФОЛЛ» выше. **Порядок:** правку `plugin.js` имеет смысл применять **вместе с ПЕРЕДЕЛКОЙ 2**, чтобы не пересоздавать контейнер дважды. ### Откат ```bash # вернуть monitored (одним запросом) curl -sL -X PUT ".../api/v3/movie/editor" -d '{"movieIds":[...],"monitored":true}' # вернуть RSS curl -sL -X PUT ".../api/v3/config/indexer" -d '{"rssSyncInterval":30,"id":1}' # восстановить базу при необходимости cp radarr.db.bak-<дата> radarr.db && docker restart radarr ``` **Статус: план на апруве. Ничего не выполнено.** ## Открытые вопросы — БЛОКИРУЮЩИЕ (2026-09-22, ждут решения Alex) 1. **Что делать с 310 фильмами, которых нет в Radarr?** В `.md` — 360, в Radarr — 50. У 310 нет TMDB ID и постера. Варианты: (а) показывать только 50 из Radarr; (б) добирать остальные через TMDB-search (часть названий найдётся криво: «девять» → не то); (в) показывать заглушкой без постера. **Без решения шаг 1 переделки не начать.** 2. **Нужны 4 секции или одна?** Секции «Фильмы / Новинки / Мультфильмы / Аниме» или одна «Мой список» сверху. Вопрос задан, ответа нет. ## Открытые вопросы (не блокируют) 1. **Постеры** — тянуть через TMDB-кэш Lampac, через Radarr `MediaCover/`, или `remoteUrl` CDN? 2. **Карточка** — `card_parse` не настроен. С реальным TMDB ID карточка должна подтянуться штатным механизмом Lampa — проверить после переделки. 3. **`🗓️` upcoming** — показывать или прятать? Сейчас показываем. 4. **`❓` ambiguous** — сейчас показываем с названием как есть. ## ⚠️ Отменённые/ошибочные указания (не повторять) - ❌ **«Настройки → Источник»** — такого пункта в Lampa **не существует**. Выдумано, Alex не нашёл. Источник переключается в разделе «остальное → основной источник» (Alex нашёл сам 2026-09-22). - ❌ **«влево → панель источников»** — тоже не подтверждено. - ❌ **`plugins_add` в `lampainit.js`** — это серверные плагины Lampac (`tmdbproxy.js`, `cubproxy.js`, `online.js`, `catalog.js`…), а **не** список плагинов Lampa. `catalog.js` там есть при `"catalog": true` в init.conf. ## Связанные - [[personal/plans/lampac-kraken]] — базовое развёртывание (шаги 1–6 сделаны) - [[personal/tech/lampac-deployment]] — эксплуатация Lampac, модуль Catalog - [[family/how-to/tv-lampac-plugin]] — подключение плагина на ТВ - [[family/projects/watchlist-sync]] — парсер списка, CLI - [[family/projects/watchlist-automation]] — форматы маркеров, cron - [[family/documents/movies-watchlist]] — источник данных ## ✅ РАБОТАЕТ (2026-09-22, конец дня) **Карточка фильма починена. Проверено: 339 из 339 карточек → HTTP 200.** ### Итог по каталогу | Секция | Элементов | Постеров | Рабочих TMDB-ID | |---|---|---|---| | Новинки | 33 | 30 | 30 | | Фильмы | 360 | 308 | 312 | | Мультфильмы | 3 | 3 | 3 | | Аниме | 7 | 7 | 7 | | **Всего** | **403** | **348** | **352** | Источники метаданных: Radarr 9, TMDB 343, Кинопоиск-фолбэк 51. ### Что сделано 1. Генератор `gen_watchlist_v2.py` — пишет **файл на каждый фильм**: `wwwroot/cards/card-movie-NNN.json`, владелец `1000:1000`, режим 644. 2. `watchlist.yaml` → `card_parse.initUrl` собирает путь из `uri`: `return $"{host}/cards/card-{uri.Replace("/", "-")}.json";`, `node: card`. 3. `deploy_lampac.py` — добавлен шаг `2b` (заливка `cards/` через tar), плюс проверка карточки в шаге 7. 4. `arr.py` — `monitored: False`, `searchFor*: False` (коммит `9872016`). 5. `plugin.js` — патч `enumerable:true`, смонтирован volume, переживает обновление образа. ### 🔴 ПИТФОЛЛЫ, найденные по ходу (все стоили времени) 1. **Проверять карточку надо по `/catalog/card`, НЕ по `/card`.** `/card` отдаёт 404 и выглядит как поломка каталога. Потерян час на ложном диагнозе. 2. **`parse.node` — СТАТИЧНЫЙ JSONPath.** `{uri}`/`{cat}` в него НЕ подставляются (`json.SelectToken(parse.node)`). Слэш в ключе `movie/NNN` трактуется как вложенность JSON → `SelectToken` возвращает null → `BadRequest("parse.node")`. Именно поэтому выбран формат «файл на фильм» с фиксированным `$.card`. 3. **`card_parse.initUrl` ИСПОЛНЯЕТСЯ как C#** (`CSharpEval`, объект `CatalogInitUrlCard` с полями `host/uri/type/args`). Только здесь можно подставить `uri`. Кавычки внутри YAML-heredoc писать экранированно. 4. **`CardController.Index(plugin, uri, type)` — параметра `cat` НЕТ.** `{cat}` в карточке всегда пустой. 5. **macOS `tar` кладёт в архив AppleDouble-мусор `._*`** — файлы попадают в `wwwroot` и мешают. Использовать `tar --no-mac-metadata` + `rm -f ._*`. 6. **Права решают.** Если файлы отдать `root:root 600` — dotnet отвечает `Permission denied`, статика молча отдаёт **404**. Нужно `1000:1000` + `755`/`644`. 7. **`find -exec` через `ssh -t` рвётся**: `missing argument to -exec`. Использовать `chmod -R` отдельными вызовами. 8. **Счётчик TMDB-ID врал нулём**: `id` приходит как `movie/424781` (префикс), а проверялось `"/movie/"` (вхождение со слэшем спереди). Проверять `startswith`. 9. **Первая категория в `menu.categories` игнорирует `format`** и берёт `list.uri` напрямую. Поэтому «Новинки» стоят первыми, и их `list.uri = watchlist-new.json`. 10. **Ключи карточки.** `CardController` отдаёт `title`/`original_title`, а `card_parse` ищет `name`/`original`. В 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 --skip-plugin # ~2 мин ``` Cron: генератор в **02:30**, после `watchlist-nightly` (01:00). ### Осталось на ТВ Проверить глазами в Lampa на `192.168.1.70`: секции сверху с постерами, клик → карточка с описанием → воспроизведение через TorrServer. Приёмка = результат виден в UI. ### Не сделано (ждёт решения, без апрува не трогать) 310 фильмов из списка, которых нет в Radarr — пока показываются через фолбэк Кинопоиска (51 без постера). Решить судьбу отдельно. --- ## 🔴 ФИНАЛЬНЫЙ ФИКС: карточка = «здесь пусто» без ЧИСЛОВОГО `tmdb_id` **2026-09-22, третья сессия. Карточка заработала: 352/352 отдают `tmdb_id`.** Alex после первого «готово»: «при открытии все так же "здесь пусто" и никакого инфо или воспроизведения». Сервер отдавал `HTTP 200` с title/img/overview — значит дело было **не в запросе, а в полноте ответа**. ### Причина — одна, но скрытая за двумя масками `plugin.js` при клике (`full()`): ```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 считает карточку неполной → пустой экран.** Причём нужен **числовой** `tmdb_id`, не строка. Два бага в цепочке `card_args`: 1. **`ModInit.cs:285` — `nodeValue` возвращает ЛЮБОЕ `JValue` как строку.** ```csharp string value = valueToken switch { JValue jValue => jValue.Value?.ToString(), // ← число 424781 → "424781" ... }; ``` Поэтому `tmdb_id` из JSON-файла приходил строкой, и `if(json.tmdb_id)` не давала числа. **Никакой конфиг это не обходит — только `format`.** 2. **Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ процесса и переживает `docker restart`.** После правки данных старые ответы продолжали отдаваться. Именно поэтому `movie/424781` (**первый элемент «Фильмов»**, по нему Alex и кликал) упорно оставался `null`, хотя исправленный файл уже лежал на диске. **Проверять фикс только на НЕзакэшированном `uri`.** ### Фикс — `format` в `card_args` (исполняется как C#, возвращает настоящий тип) ```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: overview attribute: overview - name_arg: genres attribute: genres - 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 показывает в бейдже ``` `format` вызывается как `CSharpEval.Execute(nd.format, new CatalogNodeValue(value, host), options)` (`ModInit.cs:255` и `:317`) — `value` это уже строка, поэтому нужен `TryParse`. ### Эталонный рабочий ответ ```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) отдают числовой `tmdb_id`, `source`, `runtime`, рейтинг и жанры. Ошибок — 0. ### 🔴 CUB: «вижу только фильмы из списка» CUB **не пропал** — это **два независимых источника Lampa**, одновременно показать нельзя. Переключение: **Настройки → Остальное → Основной источник**. Пункта «Источник» в настройках НЕ существует. ### Питфолл приёмки на ТВ Lampa на ТВ **кэширует манифест и ответы**. После правок надо **полностью выгрузить приложение** (не свернуть), иначе клиент держит старый кэш и показывает дореформенный результат. --- ## 🔴 СЕССИЯ 4 (2026-09-22, поздняя ночь): приёмка НЕ пройдена, требование изменено ### Что сказал Alex 1. «у фильмов в "Мой список" появились норм названия, годы и у большинства — постеры. **Но при открытии все так же "здесь пусто"** и никакого инфо или воспроизведения!» 2. «че блядь значит надо Настройки → Остальное → Основной источник?! мы блядь это уже обсуждали и **надо было добавить в CUB список новые секции** а не ручное блядь переключение!» ### Статус по факту | Пункт | Состояние | |---|---| | Серверная карточка | ✅ **исправна** — curl отдаёт `tmdb_id: 424781` числом, `source`, жанры, постер, описание | | Приёмка на ТВ | ❌ **НЕ пройдена** — «здесь пусто» на экране | | Соединения ТВ → Lampac | ❌ **ноль** (`sudo ss -tn \| grep 9118` пусто в момент проверки) | | Секции внутри CUB | ❌ не реализовано — сейчас подмена через `e.abort()` | ### Диагностика, выполненная в этой сессии Разобран `app.min.js` (2.1 МБ) — **эталон CUB-источника**: - `CUB.full()` ставит `json.source = 'cub'` сам (строка ~34252). - `Explorer` читает `this.object.movie || this.object.card`. - `full` пушится с `source: element.data.source || 'tmdb'` (**строка 44291**) — 🔴 **фолбэк на tmdb, если у элемента нет поля `source`**. В ответе `/catalog/list` поля `source` **нет** → проверить первым делом. - `plugin.js:42-43` требует от карточки: `tmdb_id` (**число**), `original_name` (или `original_title`) для определения tv/movie, `img`, `overview`. Полная анатомия — [[personal/tech/lampac-deployment]] → «Анатомия CUB-источника». ### 🎯 ЗАДАЧА СЛЕДУЮЩЕЙ СЕССИИ (переформулирована Alex'ом) **Не переключать источник, а дорисовать наши секции ВНУТРЬ CUB.** 1. **Проверить гипотезу `source`.** Элементы `/catalog/list` не содержат `source`. Если `full` уходит в `tmdb` вместо `watchlist` — это и есть «здесь пусто». Проверка: `curl "/catalog/list?plugin=watchlist&cat=films&page=1" | jq '.results[0].source'`. 2. **Отвязать плагин от перехвата `menu`.** Патчить рендер главной CUB (`app.min.js` строки ~44260-44310: `feed`/`category_full`) либо добавить ряды через `Lampa.Listener.follow('activity')`. `e.abort()` — НЕ использовать (отменяет родной раздел, чего Alex не хочет). 3. **Приёмка = глазами на ТВ.** Открыть **обычный CUB** → сверху видны наши секции с постерами → клик → карточка с описанием → кнопка воспроизведения ведёт в TorrServer. Никакого ручного переключения источника. ### Питфоллы, найденные в этой сессии - 🔴 **Проверять карточку по `/catalog/card`, НЕ по `/card`.** `/card` отдаёт 404 и выглядит как поломка каталога — потерян час на ложном диагнозе. - 🔴 **Кэш `catalog:card:{plugin}:{uri}` живёт в ПАМЯТИ** и **переживает `docker restart`**. После правки данных старые ответы продолжают отдаваться. Проверять на **незэкешированном** `uri` (взять другой фильм). - 🔴 **`nodeValue` (`ModInit.cs:285`) возвращает любое `JValue` СТРОКОЙ** → в `card_args` нужен `format: "long.TryParse(value, out long v) ? v : 0"`. - 🔴 **`tmdb_id` ДОЛЖЕН быть числом** — `plugin.js` делает `if(json.tmdb_id)`. - macOS `tar` кладёт AppleDouble `._*` в `wwwroot` → `tar --no-mac-metadata`. - Права `root:root 600` → `Permission denied` → статика молча отдаёт **404**. Нужно `1000:1000` + `755`/`644`. - `find -exec` через `ssh -t` рвётся (`missing argument to -exec`) → `chmod -R`. - `&&`-цепочка рвёт tty-сессию ssh → каждая команда отдельным вызовом. - `tcpdump` на Кра́кене **не был установлен** → `sudo apt-get install -y tcpdump`. - `com.webos.service.devtools` **не существует** на webOS 5.4.2 Lite. - `luna-send` пишет в tty → только через `script -q -c "..." /tmp/out.txt`. ### Артефакты - Скрипты: `/Users/admin/lampac-tmp/{gen_watchlist_v2.py,deploy_lampac.py,watchlist.yaml}`. - Копия UI Lampa для разбора: `/tmp/app-min.js` (2.1 МБ). - Бэкап плана: `~/backup-lampac-plugin-plan-20260922-150459.md`. - Доклады по карточке — [[personal/tech/lampac-deployment]] (конец файла). ## Связанные ## ✅ РАБОТАЕТ НА ТВ (2026-09-22, вечер) — подтверждено Alex **«Фильмы из списка Watchlist теперь открываются и проигрываются.»** ### 🔴🔴 КОРНЕВАЯ ПРИЧИНА «ЗДЕСЬ ПУСТО» — РУССКИЙ КЛЮЧ ПЛАГИНА Поймано tcpdump'ом на Кра́кене, запрос с ТВ: ``` 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 400 Bad Request ``` `plugin=Мой список` — ключ каталога брался из `displayname`. `ModInit.goInit` (строки 51-52): ```csharp site = site.ToLowerAndTrim(); site = Regex.Replace(site, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase); ``` **Кириллица вырезается ЦЕЛИКОМ** → `site = ""` → `File.Exists("/sites/.yaml")` = false → `FindSiteByDisplayName("")` тоже мимо (пустой site не совпадёт с displayname) → `return null` → **`init not found`** → HTTP 400 → в Lampa «ЗДЕСЬ ПУСТО». 🔴 **Fallback по `displayname` НЕ РАБОТАЕТ с кириллицей** — regex убивает имя раньше, чем запускается поиск. Это баг/особенность lampac-nextgen. **ФИКС:** задать `plugin: watchlist` в YAML. Строка `ApiController.cs:136`: ```csharp string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site; ``` заданный `plugin` переопределяет ключ каталога → ТВ зовёт `plugin=watchlist` → файл находится. **Приёмка:** | Ключ | Ответ | |---|---| | `plugin=Мой список` | 400 `init not found` | | `plugin=watchlist` | 200, `tmdb_id: 424781`, `source: "movie"` | ⚠️ **МЕТОДОЛОГИЧЕСКИЙ ВЫВОД (важно):** я проверял карточку своим `curl`'ом с `plugin=watchlist` и был уверен, что всё работает. **Надо было ловить РЕАЛЬНЫЙ запрос с устройства** (tcpdump на порту сервиса). Урок: при «сервер отвечает 200, а в UI пусто» — первым делом снять трафик между устройством и сервером. ### Что сделано в этом заходе 1. `plugin: watchlist` в `sites/watchlist.yaml` — ключ каталога латиницей. 2. `card_args` с `format` для числовых полей: `tmdb_id` → `long.TryParse`, `runtime` → `long.TryParse`, `vote_average` → `double.TryParse`. Причина: `ModInit.cs:285` `nodeValue` возвращает ЛЮБОЙ `JValue` как строку, а `plugin.js` делает `if(json.tmdb_id)` — нужен настоящий `long`. 3. `deploy_lampac.py`: проверка ключа `watchlist` (не «Мой список»), проверка карточек по `/catalog/card`, заливка `cards/` через tar. 4. `gen_watchlist_v2.py`: `tmdb_id` числом; дубли ключей (`name`+`title`, `original`+`original_title`, `poster`+`img`). ## ⏳ ОСТАЛОСЬ ДОБИТЬ 1. **Несколько фильмов всё ещё «здесь ничего нет»** — «Рассекая волны», «Хамнет». Похоже, для них нет `card-movie-NNN.json` (TMDB не нашёл / Кинопоиск-фолбэк без tmdb id) → `initUrl` ведёт на несуществующий файл. Лечить: фолбэк-карточка с тем, что есть (название+постер+поиск торрента), либо дотягивать tmdb_id по точному названию+году. 2. **~51 фильм без постера** — TMDB не нашёл ни напрямую, ни через Кинопоиск. Разобрать поштучно: возможно, помогают альтернативные названия (`original_title`). 3. **Секции CUB.** В «Мой список» только 3 секции (Фильмы/Мультфильмы/Аниме), «Новинки» куда-то делись? И главное — **фильмов из CUB-фида в списке нет**. Требование Alex: **секции должны быть ВНУТРИ CUB**, без ручного переключения источника. Механизм: хук `Listener.follow('menu')` в `plugin.js` (строка 59) перехватывает `movie`/`cartoon`/`anime`/`tv`/`relise` и делает `e.abort()`, НО работает только когда активен наш источник (`Api.getCatalog()` читает манифест текущего источника). Нужно доделать так, чтобы при источнике CUB наши секции добавлялись к CUB-фиду.