Files
obsidian-vault/personal/plans/lampac-watched-to-watchlist.md
T

54 KiB
Executable File
Raw Blame History

title, type, status, namespace, created, updated, tags, related
title type status namespace created updated tags related
Отметка «просмотрено» из Lampa → watchlist.md plan implemented-bug-open-markwatched-insert personal 2026-09-23T00:00:00.000Z 2026-09-27T00:00:00.000Z
lampac
lampa
watchlist
timecode
torrserver
kraken
sync-up
family/projects/watchlist-automation
personal/plans/lampac-watchlist-plugin
family/documents/movies-watchlist

Отметка «просмотрено» из Lampa → movies-watchlist.md

Проблема (Alex, 2026-09-23): «А у нас из лампы уходят отметки просмотрено в список галочками как было в jellyfin?» Ответ: нет. Задача — сделать, чтобы уходили.


1. Диагноз (проверено фактами 2026-09-23)

1.1 Канал Jellyfin мёртв — и не он нужен

  • sync_up.py умеет только Jellyfin (GET /Users/{id}/Items?IsPlayed=true) → [x].
  • Jellyfin на Кра́кене хранит BaseItems: 53 записи, movie — 0, series — 1. UserData.IsPlayed=true — 0. PlaybackActivity — 0.
  • Причина: смотреть стали через Lampa + TorrServer, а не Jellyfin. Плюс с 2026-09-22 автозакачки отключены → в Jellyfin ничего не прибавляется.
  • Вывод: Jellyfin как источник — тупик. Фактический источник просмотров — Lampa.

1.2 Lampa уже пишет просмотр на сервер — канал ЕСТЬ

Модуль Lampac TimeCode (sync):

Элемент Значение
Модуль /lampac/module/Sync/TimeCode/ (Controller.cs, plugin.js, SqlContext.cs)
БД /lampac/database/TimeCode.sql (внутри контейнера; на хосте нет — см. 8.4)
Таблица timecodes
Колонки Id, user, identity, card, item, position, duration, percent, profile, deleted, watched_at, updated_at, extra
Запись ставит Lampa → POST /timecode/add (легаси) или /timecode/set (нативный), пачками ≤10 req/s
Читает GET /timecode/all?card_id=...
Ключ личности identity = {tmdb_id}_movie — ровно то, что нужно для матча со списком

🔴 ФОРМАТ identity — ОБЕ ФОРМЫ СУЩЕСТВУЮТ. В README описана movie-674 (дефис, префикс), но card выводится как 674_movie. Живая запись показала именно дефисную форму. Regex должен принимать обе:

m = re.match(r'^(?:(movie|tv)-)?(\d+)', str(identity))

⚠️ Живая запись 2026-09-23 (снята дважды, значения менялись):

снимок 1:  identity=1758655_movie  card=movie-1758655
           position=1222.0  duration=6898.0  percent=18.0
           watched_at=0            deleted=0

снимок 2:  identity=movie-1758655  card=1758655_movie
           position=1222.0  duration=6898.0  percent=18.0
           watched_at=1790053739498   deleted=0

🔴 Вывод из снимка 2: watched_at > 0 при percent = 18. Значит watched_at ставится при любой записи таймкода, а не только при досмотре. Критерий «watched_at > 0 = просмотрено» — НЕНАДЁЖЕН. Порог должен быть по percent.

⚠️ Фильм 1758655 НЕ найден в watchlist.json (368 позиций с TMDB-ID). Это не watchlist-позиция — смотрится в Lampa из другого источника. Планируемое поведение: не найдено в json → тихо пропустить, не ошибка.

⚠️ sqlite3 в контейнере НЕТ (обычный случай у Alex) — БД читать через docker cp lampac:/lampac/database/TimeCode.sql /tmp/ + python3 файлом-скриптом (не -c, каждый inline = апрув). Готовые пробы: /tmp/probe_cache.py, /tmp/probe_match.py. На хосте Кра́кена python3 есть.

1.4 Порог «просмотрено» — ответ из документации модуля

README модуля (/lampac/module/Sync/TimeCode/README.md), раздел «Нативные клиенты»:

«Отдельного флага "просмотрено" нет: это percent. road Lampa его не несёт и не несла никогда, порог завершённости — политика клиента, а не свойство записи.»

Значит порог задаёт клиент (Lampa на ТВ), сервер его не знает. Две колонки:

Колонка Кто ставит Значение
watched_at клиент, мс Когда смотрели. Арбитр конфликтов между устройствами
updated_at сервер, мс Когда приняли строку. Курсор дельт ?since=
percent клиент Прогресс; флага «просмотрено» отдельно нет

Отсюда критерий отбора строки — 🔴 ПЕРЕСМОТРЕН по факту (см. выше):

deleted = 0   И   percent >= ПОРОГ

⚠️ deleted=1 — это снятая отметка (надгробие), position/percent обнулены, duration сохранён. Такие строки не считать просмотром. 🔴 watched_at в критерий НЕ ВХОДИТ — доказано живой записью: watched_at > 0 при percent = 18. Это просто «когда последний раз писали строку», ставится любым апдейтом таймкода.

Порог по percent — единственное надёжное. percent = 18 = начал смотреть, не досмотрел → [x] НЕ ставить. Точное значение порога (90? 95?) снять с живого досмотра: досмотреть фильм в Lampa, вычитать percent, записать факт сюда.

1.5 Что уже есть в watchlist.json — tmdb_id готов к матчу

$H/lampac/wwwroot/watchlist.json → channels.{films,new,cartoons,anime}.items[].

Поля позиции (ВСЕ, проверено):

{"name":"Простите за беспокойство","original":"Sorry to Bother You","year":2018,
 "rating":0,"image":"https://image.tmdb.org/t/p/w500/2QoxL3MMH8fvlHG7etFKfqLLnAe.jpg",
 "href":"https://www.themoviedb.org/movie/424781"}

🔴 TMDB-ID живёт ТОЛЬКО в href (.../movie/424781). Отдельного поля tmdb_id в json позиции НЕТ — это описание парсера каталога, а не json-схемы.

🔴 Поля с оригинальной строкой из .md в json НЕТ. Alex предположил, что оно есть — проверено, отсутствует. Значит матч json↔.md идёт по name + year, и обязан использовать ту же нормализацию, что parser._norm_title() (срезает markdown-ссылку, (2025), IMDb 7.1 KP 6.6, приводит к [0-9a-zа-яё]). Сырое имя не сравнивать — в .md строка выглядит так:

- [ ] ⬇️ [Под огнём](https://www.kinopoisk.ru/film/5455250/) (2025) IMDb 7.1 KP 6.6

1.6 Мост TMDB ↔ KP через .resolve_cache.json — НЕ РАБОТАЕТ

Кэш резолвера: 483 записи, все не-null. Структура:

{"status":"ok","candidate":{"tmdb_id":412656,"kp_id":null,"title":"Поступь хаоса",
 "year":2021,"media_type":"movie","score":1.0,"rating":6.548,"genres":[14,878,12]}}

🔴 kp_id = null у всех записей. Ключ существует, значение пустое. Моста TMDB↔KP в кэше нет.

Но он и не нужен. .md хранит KP-ссылки (kinopoisk.ru/film/, 68 штук), timecodes — TMDB-ID. Прямого моста действительно нет, однако цепочка замкнута через TMDB-ID, который есть с обеих сторон:

timecodes.identity (TMDB) → watchlist.json.href (TMDB) → name+year → строка .md

KP в этой цепочке не участвует вообще.

⚠️ Питфолл разведки: поиск «есть ли kp_id» по подстроке в JSON-блобе дал ложное «458 записей с kp-полем» — потому что строка "kp_id" присутствует с null. Проверять значение, а не наличие ключа.


2. Решение

Новый модуль в watchlist-sync: sync_from_lampac.py. Читает timecodes → сопоставляет с watchlist.json по TMDB-ID из href → берёт name + year → находит строку .md по нормализованному названию → ставит [x] тем же sync_up._mark_watched, что уже работает для Jellyfin. Вызывается в конце watchlist-nightly.sh — после sync-up, перед sync-vault (push).

Цепочка матча (единственная рабочая):

timecodes.identity   "movie-1758655" / "1758655_movie"
        │  regex ^(?:(movie|tv)-)?(\d+)
        ▼
   tmdb_id = 1758655
        │  watchlist.json: href = ".../movie/1758655"
        ▼
   {name, year}  →  нормализация + год
        ▼
   строка movies-watchlist.md  →  [x]

KP-ID (kinopoisk.ru/film/, 68 штук в .md) в цепочке не участвует — моста TMDB↔KP нет и не требуется.

Почему так, а не «писать в Jellyfin»: Jellyfin не источник просмотров и не имеет файлов для большинства позиций. Единственный честный источник — Lampa.

Статус на конец сессии 2026-09-23:

  • ✅ Разведка закрыта, цепочка матча проверена живой записью.
  • ✅ План написан, исправлен по фактам (порог, хостовый путь БД, .14/.15).
  • ✅ Закоммичены висевшие незакоммиченными фиксы сирот (ffe7974).
  • ✅ DNS kraken → .14 РАБОТАЕТ (без ребута роутера, см. 8.3.1) — блокер снят.
  • ✅ Проверка стека после ребута: 14/14 контейнеров, queue 0 (см. 8.1).
  • ❌ Код sync_from_lampac.py НЕ написан.
  • ⏳ Блокер: точный порог percent требует живого досмотра в Lampa (см. §7) — без него шаг 6 не закрыть.
  • ⏳ Не сделано: бэкап (шаг 1), .env (шаг 4), крон (шаг 5), доки (шаг 7).

10. ✅ РЕАЛИЗАЦИЯ 2026-09-24 (коммит ef44f3e)

Модуль написан, протестирован, встроен в nightly. Все 8 шагов плана закрыты (кроме калибровки порога — см. ниже).

Что сделано

Шаг Результат
8 (матч) Проверен синтетикой: обе ветки работают (cache точный + json фолбэк)
1 (бэкап) ~/backup-watchlist-before-lampac-sync-20260924-005630/ (TimeCode.sql + .md); репо — git push
2 (код) watchlist_sync/sync_from_lampac.py + команда sync-from-lampac в cli.py
3 (dry-run) Синтетическая БД (8 сценариев) + копия .md → diff ровно 2 ожидаемые строки
4 (.env) LAMPAC_TIMECODE_DB, LAMPAC_WATCHLIST_JSON, LAMPAC_PERCENT_THRESHOLD=90 (бэкап .env.bak-lampac-20260924)
5 (крон) Врезан в ~/scripts/watchlist-nightly.sh после sync-up, до vault push; md5 новой edf0e34530692dbb7b6fb9644f1b45a7, бэкап .bak-lampac-20260924; полный ручной прогон 01:05–01:07 ✓
6 (порог) percent >= 90 (env/CLI --percent); точная калибровка — после живого досмотра
7 (доки) Этот план + family/projects/watchlist-automation.md + скилл watchlist-sync-resolver

Матч-цепочка (реализована двухступенчатая)

timecodes.identity ('movie-674' / '674_movie' — строгие формы, иначе скип)
  → tmdb_id
  → 1) .resolve_cache.json: candidate.tmdb_id → ключ кэша = заголовок .md (ТОЧНО)
  → 2) фолбэк watchlist.json: href '/movie/<id>' → name (TMDB ru) / original (en)
       + сверка года (расхождение > 1 → пропуск с логом)
  → _mark_watched (как Jellyfin-канал; ⬇️ срезается, эмодзи-префикс сохраняется)

Тестовая матрица (все 8/8 ✓)

Сценарий Ожидание Факт
cache-матч, percent=95 [→WL] via cache ✓ («Поступь хаоса», 412656)
json-матч, percent=97 [→WL] via json:name ✓ («Смерть единорога», 1153714)
percent=18 скип ниже порога ✓
tv-1234-s1e2 скип tv ✓
deleted=1 скип ✓
уже [x] фильм [ok] ничего не делаю ✓ (63958)
левый id 999999999 [miss] вне watchlist ✓
dry-run без --apply 0 записей в .md ✓

🔴 Побочный баг найден и починен: _normalize в sync_up.py

_normalize не делал .strip() после удаления небуквенных символов: 'Смерть единорога ⭐' → 'смерть единорога ' (хвостовой пробел) ≠ 'смерть единорога'. Из-за этого _mark_watched молча проваливался на заголовках с ⭐/✭ на конце — включая канал Jellyfin (там баг жил незамеченным). Фикс: return t.strip(). Симптом до фикса: [!] _mark_watched не нашёл строку для 'Смерть единорога'.

Новые грабли этой сессии

# Грабля Обход
14 timecodes UNIQUE(user, identity) — повторный INSERT той же пары падает одна строка на пользователя+фильм, percent обновляется на месте; в тесте — разные user
15 Sync.sql (bookmarks с categories) и Sisi.sql — ПУСТЫЕ (проверено 2026-09-24) Lampa не синхронизирует закладки на Lampac; TimeCode — единственный канал
16 json.name = TMDB ru-заголовок (требование «как в tmdb/kp»), ≠ строке .md двухступенчатый матч: resolve-cache точный → json name/original + год
17 длинный inline-ssh с URL-подобными строками уходит на апрув tirith и истекает файл-скрипт через scp (как всегда)

🔴 ИНЦИДЕНТ 2026-09-24 10:22 — риск §8.4 материализовался

  • Watchtower нашёл новый образ ghcr.io/lampac-nextgen/lampac:latest (04:11 UTC) и пересоздал контейнер в 10:22:51–10:23:04 (+06). За сессию обновлены 4 контейнера: lampac, radarr, transmission, prowlarr.
  • /lampac/database не на volume → врайтабл-слой стёрт, TimeCode.sql создан заново пустым (10:36). История просмотров уничтожена.
  • Бэкап от 00:56 (~/backup-watchlist-before-lampac-sync-20260924-005630/TimeCode.sql) проверен: единственная запись movie-1758655 (18%, вне списка). Просмотр «Мастерской Сатаны» (movie-1159596) шёл после 00:56 → запись уничтожена в 10:22, восстановления нет.
  • Симптом у Alex: «Мастерская сатаны почему не отметился просмотренным?»
  • Модуль TimeCode в новом контейнере загрузился — канал жив, новые просмотры пишутся в новую БД.
  • Урок: риск «ждёт апрува» ≠ риск закрыт. Ежедневный бэкап (docker cp → ~/backup/timecode/<дата>) не требовал никакого апрува и должен был быть в nightly с 09-24. Риск в доке без бэкапа — это наблюдение за бомбой, а не её разминирование.
  • Fix (ждёт команды Alex): bind-mount database/ + строка бэкапа в nightly. До bind-mount КАЖДОЕ автообновление lampac сносит историю.

✅ FIX ВЫПОЛНЕН 2026-09-24 13:38–13:50 (по команде «чини»)

Что Детали
bind-mount ./database:/lampac/database в /srv/dev-disk-by-uuid-6194539b-.../lampac/docker-compose.yml; каталог создан install -d -o 1000 -g 1000 (uid лампака = 1000 = kraken на хосте). Бэкап compose: docker-compose.yml.bak-dbmount-20260924-1100. Контейнер пересоздан docker compose up -d lampac
Проверка mount в inspect ✓, TimeCode.sql+WAL создались на хосте ✓, loaded module: TimeCode ✓, HTTP :9118 → 200 ✓
Бэкап в nightly Блок ПЕРЕД sync-from-lampac: sqlite3 <host>/TimeCode.sql ".backup ~/backup/timecode/TimeCode-$(date +%F).sql" + retention 30 дней. md5 нового ~/scripts/watchlist-nightly.sh = afabbb82f1234496521083ebd7b19eaf. Прогон вручную ✓ (снапшот 24КБ со схемой)
.env LAMPAC_TIMECODE_DB → хост-путь /srv/.../lampac/database/TimeCode.sql (модуль читает напрямую, без docker cp). Бэкап .env.bak-dbmount-20260924. Dry-run модуля ✓ (0 записей — ожидаемо, БД свежая)
Репо коммит 4266e37 (устаревший комментарий «БД НЕ bind-mounted»), запушен, на Кракене fast-forward ✓

🔴 Грабля №18 (новая, найдена при фиксе): SQLite WAL. БД TimeCode живёт в WAL-режиме: свежие данные — в TimeCode.sql-wal, главный .sql может быть пустой оболочкой (проверено: docker cp одного файла → no such table). Голый cp/docker cp одного .sql — НЕ бэкап. Чтение — только (а) напрямую из каталога со всеми тремя файлами (main+wal+shm — теперь так делает модуль через хост-путь), (б) sqlite3 X ".backup Y" — консистентный однофайловый снапшот (так делает nightly).

Восстановление «Мастерской Сатаны» [x]: записи больше нет; канал проставит галку сам при повторной отметке — открыть фильм в Lampa и перемотать на финальные титры (порог percent>=90 заодно откалибруется).

Осталось (не блокирует работу канала)

  • ⏳ Калибровка порога: досмотреть фильм из вотчлиста в Lampa → вычитать percent из timecodes → если ≠ 90, поправить LAMPAC_PERCENT_THRESHOLD в .env (перезапуск не нужен, читается при каждом запуске).
  • ⏳ Сериалы (tv-*) — этап 2.
  • ⏳ Риск /lampac/database не на volume — закрыть bind-mount'ем (отдельный апрув).
  • Проверка в UI: после первого реального досмотра фильм должен пропасть из «Мой список» в Lampa после ночного прогона (каталог 02:30 пересобирается и выкидывает [x]).

Открытые пункты, ждущие команды Alex (обновлено 2026-09-24):

  1. Лог nightly: /tmp/watchlist-nightly.log → /home/kraken/logs/ (одна строка crontab, см. 8.2).
  2. arr.py: searchForMovie: False — снято с повестки: 2026-09-24 Alex вернул автозакачку (True/True, коммит ef7e7af; вручную добавленные качаются сразу). См. family/projects/watchlist-automation § «2026-09-24: защита СНЯТА».
  3. sync_from_lampac.py — ✅ сделано 2026-09-24: коммит ef44f3e, встроен в nightly.

Порядок следующей сессии: шаг 8 (прогнать /tmp/probe_match.py, убедиться что матч находит хоть одну позицию) → шаг 1 (бэкап) → шаг 2 (код) → шаг 3 (dry-run на копии) → шаги 4-7. ⚠️ Шаг 8 стоит ПЕРВЫМ не случайно: если матч даёт 0 совпадений, писать код бессмысленно — сначала чинить матч.


11. 🔴 2026-09-27 — ДИАГНОЗ: [x] НЕ падает вниз списка (баг _mark_watched)

Симптом Alex: «Почему просмотренные в watchlist типа Даун Хаус или Кокаиновый медведь не падают вниз списка? Да и новинки как будто давно не обновлялись»

11.1 Корень: вставка в НАЧАЛО, а не в конец блока просмотренных

watchlist_sync/sync_up.py:69 внутри _mark_watched:

insert_at = next((i for i, l in enumerate(lines) if re.match(r'\s*-\s*\[x\]', l)), None)
lines.insert(insert_at, raw)

next(...) = первая [x]-строка файла → вставка идёт выше неё. Комментарий докстринга обещает move to top of [x] section — фактически top of file. Расхождение кода и комментария и есть баг.

Факт из файла 27.09: - [x] Даун Хаус — строка 326, окружена [ ]-соседями (324/325, 327/328) → уехала вверх на одну позицию и застряла. Строки 106 и 109 (Кокаиновый медведь, Проект конец света) — тот же случай.

Блок просмотренных реально начинается на строке 421 (Бойцовский клуб) и кончается на 674. Между позицией строки и блоком — ~130 строк со [ ] (11 секций, включая ## Мультфильмы на 676). Строка при вставке «по одной» эту дистанцию не проходит никогда.

Аудит: 178 [x]-строк в главных секциях, из них 4 выше блока 421–674 — все четыре пришли от sync-from-lampac после 25.09. Остальные 174 уже стояли в блоке, и вставка «выше первой [x]» для них работала правильно.

11.2 Готовый инструмент уже в коде

Точная функция для правки существует: parser.py:401 find_main_last_watched(ls) — находит последний [x] в главных секциях (исключая Новинки/Новинки👎). Её использует только process_thumbs (parser.py:420: insert_at = last_watched + 1). _mark_watched её не зовёт — отсюда баг.

Фикс (НЕ применён, ждёт команды Alex): ~5 строк в sync_up.py — заменить next(...) на parser.find_main_last_watched(lines) + insert_at = idx + 1. Тест: копия .md, сверка diff, только потом заливка. Порядок правок штатный: коммит ДО → правка → заливка → проверка Alex → коммит ПОСЛЕ.

11.3 ⚠️ Расхождение доков и кода: пути БД

family/projects/watchlist-automation.md (раздел 2026-09-24) описывает матч так:

→ _mark_watched ([x], строка уезжает в блок просмотренных)

и «БД TimeCode.sql — только внутри контейнера lampac (НЕ на volume!)».

Проверено 27.09 — устарело: БД живёт на хосте по bind-mount'у (исправлено 2026-09-24 13:38, см. §10 «FIX ВЫПОЛНЕН»): /srv/dev-disk-by-uuid-6194539b-…/lampac/database/TimeCode.sql. Строку про «только в контейнере» в watchlist-automation.md надо поправить.

Строка «строка уезжает в блок просмотренных» фактом не подтверждается — см. 11.1. Это то же расхождение, что и баг: док писался по замыслу, а не по коду.

11.4 Второй симптом: «новинки давно не обновлялись» — тут ДВА разных «не обновлялись»

Что Факт 27.09
Каталог Lampa ✅ обновляется каждую ночь: watchlist.json — 27.09 01:07, count: 403 (films 357 / new 32 / cartoons 7 / anime 7). Бэкап TimeCode.sql за 27.09 в 01:09 создан
Секция ## Новинки в .md 🔴 последний discover был 21.09. Крон стоит (0 9 * * 0), но в логе watchlist-discover.sh за 27.09 (воскресенье) — только sync-vault pull и push, шага discover нет

Плюс структурная причина: discover кладёт новинки в ## Новинки - 👍/👎, а sync_down.py:70 держит эту секцию в HIDE_SECTIONS = {"Новинки👎", "Связанные заметки"} (и SECTION_TO_CODE для "Новинки - 👍/👎" → new, но фильтр HIDE_SECTIONS её не отсекает — в канале new на 27.09 лежит 32 позиции). То есть новые «новинки» могут появиться в .md, но в фид Lampa попадают только после 👍.

11.5 Побочная находка: push раньше pull

В логе watchlist-discover.sh за 27.09 sync-vault push (10:21:08) идёт на 5 минут раньше последнего pull. По таймингу не совпадает ни с ночным прогоном (01:00), ни с расписанием discover (09:00) — либо ручной прогон, либо повторный запуск. На данные не повлияло, но порядок pull/push в скрипте стоит проверить.

11.6 Что решено / что ждёт

Ждёт команды Alex (правки кода не делались):

# Дефект Правка
1 [x] вставляется в начало вместо конца блока sync_up.py, ~5 строк, замена на find_main_last_watched
2 discover не запускался 27.09 до правки крона — bash -x скрипта + его содержимое
3 4 «застрявшие» [x] (106/109/111/326) вручную нельзя (правило «маркеры только скриптами»). После фикса №1 они съедут сами только при повторном _mark_watched для той же строки — для них вызова не будет. Варианты: оставить до следующего синка, либо одноразовый проход sync-up по уже-[x] строкам (новая функция, отдельный апрув)

11.7 Питфоллы диагностики этой сессии

# Грабля Обход
19 Логи крона в /tmp/watchlist-nightly.log не читаются — файла нет (см. §8.2, /tmp чистится ребутом). Прогон восстанавливается только по mtime артефактов проверять артефакты: watchlist.json mtime, ~/backup/timecode/TimeCode-<дата>.sql, размер .md по коммитам
20 git log --oneline показывает только последний коммит дня — «изменений нет» по diff'у с HEAD не значит, что nightly не работал сверять размер .md по коммитам (git show <c>:path | wc -c); ночной прогон виден как бамп 30696 → 30802
21 jq '.channels.films | length' даёт 2 (ключи items/title), а не число позиций .channels.films.items | length → 357

3. Шаги

  1. Бэкап. TimeCode.sql + movies-watchlist.md + watchlist-sync/ в ~/backup-watchlist-before-lampac-sync-<ts>/. Без бэкапа не начинать.
  2. Новый файл watchlist_sync/sync_from_lampac.py:
    • читает БД по пути из env LAMPAC_TIMECODE_DB (default /lampac/database/TimeCode.sql; хост-путь подставить в .env);
    • отбирает deleted = 0 И percent >= ПОРОГ (см. 1.4 — критерий пересмотрен: по percent, НЕ по watched_at);
    • identity → tmdb_id; форматы movie-674, 674_movie, tv-1851-s1e2; на этапе 1 только фильмы, tv-* пропускать;
    • position/duration/percent выводить в лог для отладки — по ним видно, насколько клиент реально досмотрел;
    • строит {tmdb_id: title} из watchlist.json (channels.*.items[]);
    • для каждого найденного [ ]-строки в .md — [x] через _mark_watched;
    • --dry-run по умолчанию, --apply для записи; печатает каждое действие.
  3. Копия для теста. Dry-run на копии боевого .md, сверить: сколько строк перешло, какие именно. Только после сверки — --apply на боевом.
  4. .env на Кра́кене: LAMPAC_TIMECODE_DB=/lampac/database/TimeCode.sql — 🔴 путь ВНУТРИ контейнера, хостовой копии нет (см. 8.4). Модуль запускается на хосте Кра́кена, поэтому перед чтением делать docker cp lampac:/lampac/database/TimeCode.sql <tmp> и читать копию. Либо (предпочтительно, отдельным апрувом) bind-mount database/ на хост.
  5. Крон. В ~/scripts/watchlist-nightly.sh — новый вызов после sync-up, до sync-vault (push). Правка через sudo cp (~/scripts/ — root:root 755). Бэкап скрипта до правки. Сумма новой версии зафиксировать в доке.
  6. Порог. 🔴 По percent, НЕ по watched_at — пересмотрено по факту (см. 1.4): watched_at > 0 встречается при percent = 18. Стартовое значение percent >= 90. Уточнить с живого досмотра: досмотреть в Lampa, вычитать percent, вписать факт сюда. percent = 18 проходить не должен.
  7. Документировать в family/projects/watchlist-automation.md (раздел про синк)
    • ссылка из personal/plans/lampac-watchlist-plugin.md.
  8. Проверка матча — обязательна до записи. Прогнать /tmp/probe_match.py (цепочка timecodes → watchlist.json → .md) и убедиться, что хоть одна позиция находится. Если находится 0 — матч сломан, --apply не запускать. Сейчас: 1758655 в json не найден (вне watchlist) → 0 совпадений ожидаемо.

4. Грабли (известные заранее)

# Граблю Обход
1 sqlite3 в контейнерах НЕТ docker cp + файл-скрипт python3 /tmp/x.py на хосте
2 ~/scripts/ на Кра́кене root:root 755 — scp даёт Permission denied только sudo cp
3 Кириллица в inline-ssh BLOCKED tirith — писать скрипт файлом (write_file), не heredoc
4 identity для сериалов = tv-1851-s1e2, серия в card этап 1 — только фильмы; сериалы отдельно
5 percent сбрасывается в 0 при deleted фильтровать deleted = 0
6 Матч по названию ломается на год/рейтингах нормализация _norm_title() + год как второй ключ
7 🔴 /lampac/database НЕ на volume и НЕ на хосте — БД только внутри контейнера; пересоздание контейнера уничтожит историю читать через docker cp; риск потери закрыть bind-mount'ом отдельно (см. 8.4)
8 Крон-файлы возвращать только crontab /tmp/файл; бэкап crontab -l > f обязателен —
9 🔴 identity бывает в ДВУХ формах (movie-674 и 674_movie) regex ^(?:(movie|tv)-)?(\d+)
10 🔴 watched_at не признак досмотра — бывает >0 при percent=18 порог только по percent
11 🔴 inline python3 -c уходит на апрув и истекает все пробы — файлами
12 ✅ .15 и .14 — ОДИН Кра́кен, оба живые. Прежнее «.15 мёртвый» — моя ошибка обрывы = перегрузка sshd; короткие команды, повторы
13 Длинные ssh + тяжёлый jq рвут соединение («banner exchange») разбивать на короткие команды, тяжёлое — файлом

5. Проверка результата

  • sync_from_lampac.py --dry-run находит запись 1758655_movie (18% — не проходит)
  • После ручной отметки фильма в Lampa до 100% → --dry-run показывает его, --apply ставит [x]
  • [x]-строка переехала вверх (поведение _mark_watched как у Jellyfin-канала)
  • Лог ночного прогона содержит строку от sync_from_lampac
  • Alex видит: фильм, отмеченный в Lampa, пропал из «Мой список» в каталоге после ночного прогона
  • git log в vault — коммит с изменённым .md от Hermes

6. Что НЕ делается (по состоянию на 2026-09-24)

  • Не трогаем arr.py — 2026-09-24 Alex вернул автозакачку: True/True, коммит ef7e7af (подробности — family/projects/watchlist-automation).
  • Не возвращаем sync-down в крон — остаётся вырезан и после снятия защиты: авто-добавлений из watchlist нет, качается только вручную добавленное.
  • Не пишем в Jellyfin — он не источник просмотров.
  • Не трогаем cleanup — отдельная задача.

7. Открытый вопрос — ЗАКРЫТ документацией ✅

Alex сказал «в душе не ебу как она там ставится, ищи документацию» — и был прав. Ответ найден в README модуля, вопрос задавать не следовало.

README TimeCode, раздел «Нативные клиенты»:

«Отдельного флага "просмотрено" нет: это percent. road Lampa его не несёт и не несла никогда, порог завершённости — политика клиента, а не свойство записи.»

То есть порог — политика клиента (Lampa на ТВ), сервер его не знает и не хранит. Отсюда: единственный переносимый признак — percent, и значение порога надо взять у клиента, а не у сервера.

Дополнительно опровергнуто фактом: живая запись дала watched_at > 0 при percent = 18. Значит watched_at — «когда последний раз писали строку», а не «досмотрел». Использовать его как критерий — ошибка.

Как снять точный порог (следующая сессия):

  1. Досмотреть в Lampa фильм из вотчлиста (не 1758655 — его нет в json).
  2. Вычитать percent из timecodes (через /tmp/probe_match.py).
  3. Вписать наблюдённое значение в этот раздел и в шаг 6.

8. Связанные находки этой сессии (не часть плана, но проверено)

8.1 Стек после ребута

14/14 контейнеров поднялись сами, Radarr/Sonarr queue 0, Transmission 9 торрентов всё Done/Idle (Sum down 0.0), диск 31% (539G/1.8T), Lampac :9118 → 200, Jellyfin :8096 → 302. Ночные скрипты отработали: cleanup 02:00, nightly 01:00, catalog 02:30–02:32 (каналов 4, позиций 392, .md 708 строк / 615 позиций). Vault git чистый. Записано в family/projects/watchlist-automation.

8.2 🔴 Лог nightly живёт в /tmp/ → умирает при ребуте

Крон: 0 1 * * * bash ~/scripts/watchlist-nightly.sh >> /tmp/watchlist-nightly.log. /tmp чистится при перезагрузке — сегодняшний прогон следа не оставил, вывод восстановлен только косвенно (по mtime артефактов). Остальные логи уже в ~/logs/ (watchlist-catalog.log, watchlist-cleanup.log). Фикс (перенести в ~/logs/) предложен, ждёт команды Alex.

8.3 ✅ Уточнено: .14 = провод, .15 = WiFi. Оба — один Кра́кен

🔴 Изменено 2026-09-23 (поздняя правка). Предыдущая версия этого раздела утверждала «.15 мёртвый» — ЭТО НЕВЕРНО, ошибка снятия одного таймаута за факт.

Alex уточнил: 192.168.1.15 — WiFi (wlan0), 192.168.1.14 — провод (eth0). Ходим по .14. Термин «мёртвый» был моим ложным выводом.

Проверено:

$ ping -c 2 192.168.1.15        → 0% loss, 5.5 ms avg
$ ssh kraken@192.168.1.15       → hostname = kraken
        hostname -I = 192.168.1.14 192.168.1.15 10.99.1.2 172.21.0.1 …
$ ssh kraken@192.168.1.14       → ALIVE_14

на Кра́кене (актуально на 2026-09-23, после отключения WiFi):
eth0   UP  192.168.1.14/24  metric 100   ← основной, default route
wlan0  DOWN                              ← отключён 2026-09-23 (см. 8.3.1)

Кра́кен имеет оба MAC (…:f3 eth0, …:f4 wlan0) — это одна и та же машина.

Настоящая причина обрывов — не адрес, а перегрузка sshd: первые подключения падали при load average: 4.69 (после ребута поднимались контейнеры). Симптом: Connection closed by UNKNOWN port 65535 / Connection timed out during banner exchange. Лечится повтором и короткими командами, а не сменой адреса.

⚠️ Дополнительный источник обрывов (подтверждён 2026-09-23): ЛЮБАЯ команда с sudo через SSH требует < /dev/null, иначе соединение рвётся. См. family/how-to/truenas-infrastructure.md / skill home-network-config.

Обход, который реально помогает:

  • короткие ssh-команды, тяжёлое — файлом-скриптом;
  • не собирать ssh + тяжёлый jq в одну строку (180 KB json = обрыв);
  • sudo всегда с < /dev/null.

⚠️ WG-туннель до Кра́кена не поднят: интерфейса с 10.99.0.2 на Mac нет, nc 10.99.1.2 22 → FAIL. Proxy-команда в ~/.ssh/config уходит в ветку nc kraken 22. Само по себе это не ломает (Кра́кен доступен по LAN), но объясняет, почему прокси не используется.

8.3.1 🔴 DNS-имя kraken на роутере указывало на WiFi .15 — ✅ РЕШЕНО БЕЗ РЕБУТА

Симптом: ssh kraken (по имени) шёл на .15 — то есть на WiFi-интерфейс. Когда он оказывался DOWN, ssh kraken переставал работать вовсе.

Причина — ДВА конфликтующих источника имени на роутере ASUS RT-AC68U:

$ nvram get dhcp_staticlist
<12:42:0C:E6:31:0F>192.168.1.51>orangepizero>
<B0:6E:BF:60:6D:64>192.168.1.86>htpc>
<84:25:19:74:A6:B3>192.168.1.80>>samsung_printer
<54:77:87:CE:FB:B3>192.168.1.75>lgtv>
<2C:CF:67:64:03:F3>192.168.1.14>kraken>      ← eth0, ИМЯ kraken
<2C:CF:67:64:03:F4>192.168.1.15>kraken>      ← wlan0, ИМЯ kraken (дубль!)

$ cat /jffs/configs/dnsmasq.conf.add
address=/kraken/192.168.1.15                 ← жёстко на WiFi

Оба интерфейса несли одно имя kraken, плюс статический address= на .15.

🔴 Ключевой питфолл: живая DHCP-лиза ПЕРЕБИВАЕТ address=. Активная лиза wlan0 с именем kraken (/var/lib/misc/dnsmasq.leases) приоритетнее статической записи. Удаление строки лизы не помогает — dnsmasq перезаписывает её из nvram при рестарте, а клиент при DHCP-запросе сам представляется kraken (hostname хоста один на оба интерфейса).

Что сделано на роутере (2026-09-23):

# 1. Бэкапы
nvram get dhcp_staticlist > /jffs/dhcp_staticlist.bak.20260923-214309
cp /jffs/configs/dnsmasq.conf.add /jffs/configs/dnsmasq.conf.add.bak.20260923-214309
cp /var/lib/misc/dnsmasq.leases /jffs/dnsmasq.leases.bak.<ts>

# 2. Убрано дублирующее имя у wlan0-лиза (ip остался)
nvram set dhcp_staticlist="…<2C:CF:67:64:03:F3>192.168.1.14>kraken><2C:CF:67:64:03:F4>192.168.1.15>>"
nvram commit

# 3. Статический address переведён на провод
# /jffs/configs/dnsmasq.conf.add:
address=/kraken/192.168.1.14                 ← было .15
service restart_dnsmasq

Результат: dig +short kraken @192.168.1.1 → 192.168.1.14 ✅. Проверено: ping идёт, ssh kraken@kraken заходит, wlan0 DOWN, .15 не выдаётся.

🔴 Ребут роутера НЕ ПОНАДОБИЛСЯ — ни nvram, ни перезагрузка. Настоящая причина «залипшего» .15 оказалась в другом.

🔴 НАСТОЯЩАЯ ПРИЧИНА (стоила часов — не повторять)

Чистка лиз-файлов (4 раза) и service restart_dnsmasq (многократно) не помогали. Причина — две независимые вещи:

  1. Рабочий лиз-файл dnsmasq — /tmp/var/lib/misc/dnsmasq.leases, а НЕ /var/lib/misc/. Чистил не тот файл. Определить рабочий:
    for p in <PID1> <PID2>; do echo "PID $p:"; ls -l /proc/$p/fd | grep leases; done
    # → /tmp/var/lib/misc/dnsmasq.leases
    
  2. dnsmasq на ASUSWRT работает ДВУМЯ процессами (ps w | grep dnsmasq → nobody + admin), и service restart_dnsmasq их не убивает — старый ответ .15 остаётся в памяти процесса. Перечитывание конфига не помогает, пока процесс жив.

Что сработало:

# 1. почистить РАБОЧИЙ (tmp) лиз-файл
ssh admin@192.168.1.1 'grep -v "192.168.1.15" /tmp/var/lib/misc/dnsmasq.leases > /tmp/l.new \
  && cat /tmp/l.new > /tmp/var/lib/misc/dnsmasq.leases'
# 2. ЖЁСТКО убить процессы — restart_dnsmasq их не убивает!
ssh admin@192.168.1.1 'killall -9 dnsmasq; sleep 2; service restart_dnsmasq; sleep 6'
# 3. проверить
sudo dscacheutil -flushcache; dig +short kraken @192.168.1.1   # → 192.168.1.14

Таблица «что НЕ помогало» (не повторять)

Действие Почему бесполезно
Удаление лизы из /var/lib/misc/dnsmasq.leases Не тот файл — рабочий в /tmp/var/lib/misc/
service restart_dnsmasq Не убивает процессы, память сохраняется
killall -SIGHUP dnsmasq Не сбрасывает DNS-кэш ответов
dhcp-ignore-names=lan Неверный синтаксис ASUSWRT (нужен tag:) — строку убрать
/jffs/scripts/dnsmasq.postconf Не нужен: ASUSWRT читает conf.add нативно, скрипт давал дубль блока в /etc/dnsmasq.conf
ip link set wlan0 down на Кра́кене systemd-networkd поднимает обратно (Activation Policy: up)
netplan с dhcp4: false для wlan0 Не применяется — [Network] DHCP=ipv4 из дефолтного 10-netplan-wlan0.network перебивает

⚠️ Ребут роутера из агента НЕВОЗМОЖЕН: reboot в жёстком блоклисте Hermes (BLOCKED (hardline) — не апрув, обойти нельзя ни --yolo, ни другим синтаксисом). Этот фикс обходит необходимость ребута. /sbin/rc на ASUSWRT — Segmentation fault, как обход не годится.

⚠️ pgrep и rfkill на роутере и на Кра́кене ОТСУТСТВУЮТ (command not found) — использовать ps w | grep, for p in <PID> вручную.

WiFi wlan0 — ОТКЛЮЧЁН (2026-09-23)

wlan0 сейчас DOWN, адрес .15 снят. Чтобы погасить настойчиво возвращающийся интерфейс:

# правка /etc/netplan/30-wlan0.yaml → dhcp4: false + optional: true
sudo netplan apply < /dev/null
sudo iw dev wlan0 disconnect < /dev/null   # снять ассоциацию с AP ASUS5
sudo ip link set wlan0 down < /dev/null

⚠️ Держится до перезагрузки Кра́кена ([Link] Unmanaged в netplan не сработал — 99-default.link перебивает). После ребута проверить ip -4 addr show wlan0.

Бэкапы на роутере: /jffs/dhcp_staticlist.bak.20260923-214309, /jffs/configs/dnsmasq.conf.add.bak.20260923-214309, /jffs/configs/dnsmasq.conf.add.bak3.<ts>. Скилл home-network-config § «РЕШЕНО БЕЗ РЕБУТА» — тот же материал.

8.4 🔴 TimeCode.sql НЕ на хосте — только внутри контейнера, и НЕ на volume

🔴 Критично для реализации. Предыдущая версия плана называла хостовый путь $H/lampac/database/TimeCode.sql — такого пути НЕТ.

Факт:

$ docker inspect lampac --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}\n{{end}}'
…/lampac/cache      -> /lampac/cache
…/lampac/config     -> /lampac/config
…/lampac/data       -> /lampac/data
…/lampac/config-module/init.conf -> /lampac/init.conf
…/lampac/catalog/override, plugin.js, sites -> /lampac/module/Catalog/*
…/lampac/wwwroot   -> /lampac/wwwroot

/lampac/database в маунтах ОТСУТСТВУЕТ. Файл живёт внутри контейнера:

docker exec lampac ls -la /lampac/database/TimeCode.sql
-rw-r--r-- 1 lampac lampac 24576 Sep 22 15:32 /lampac/database/TimeCode.sql

Способы чтения (единственный рабочий):

docker cp lampac:/lampac/database/TimeCode.sql /tmp/TimeCode.sql
python3 /tmp/probe_match.py          # файлом, НЕ -c

🔴 РИСК ПОТЕРИ ДАННЫХ. /lampac/database не на volume → docker compose up --force-recreate, обновление образа ла́мпака или пересоздание контейнера уничтожат всю историю просмотров. Прямо сейчас там 1 запись (потери нет), но план зависит от этой БД — риск закрыть отдельным шагом (bind-mount database/ на хост, с бэкапом перед правкой compose, без пересоздания контейнера без разрешения Alex).

8.5 Commit незакоммиченных фиксов сирот

В рабочем дереве watchlist-sync висели незакоммиченные правки, которые уже работали боевым прогоном: cli.py (+7) и parser.py (+65) — фикс «ОСИРОТЕВШИЕ СТРОКИ» в process_thumbs() + явные ветки вывода 👍/👎/[x]/[drop]. Закоммичены отдельным коммитом ffe7974 (не смешаны с новым кодом).

_mark_watched в sync_up.py уже содержит фикс эмодзи-префикса (_PREFIX поглощает эмодзи с обеих сторон ⬇️) — переиспользуется без правок.

8.6 Внешний TMDB API с Кра́кена недоступен

curl https://api.themoviedb.org/3/movie/... без ключа → Invalid API key. Ключ живёт в $WS/.env (TMDB_KEY), прямой вызов из шелла не проходит. Для проверки названий использовать watched_at-путь через watchlist.json, а не TMDB API.

8.7 Проверка API Radarr/Sonarr — рабочая последовательность

Проверено 2026-09-23. Ключ читать из контейнера, не с хоста (на хосте неочевидный путь, легко промахнуться):

sudo docker exec radarr cat /config/config.xml < /dev/null | grep -o '<ApiKey>[^<]*'
# надёжнее — с диска, без docker exec:
grep -o '<ApiKey>[^<]*' <HOST>/docker/radarr/config/config.xml | cut -d'>' -f2
curl -s --max-time 10 "http://localhost:7878/radarr/api/v3/queue?pageSize=50" \
  -H "X-Api-Key: $RK" | jq -r '.totalRecords'
# то же на 8989 для sonarr

⚠️ docker exec без < /dev/null рвёт SSH-сессию (тот же питфолл, что с sudo). Скрипт целиком передавать так: ssh kraken 'bash -s' < /tmp/script.sh — работает устойчиво, в отличие от длинных inline-команд.


9. Связанный артефакт — скилл home-network-config

Весь материал §8.3.1 (причина «залипшего» .15, /tmp-лиз-файл, двойной процесс dnsmasq, таблица «что не помогало», способ без ребута) продублирован в скилл software-development/home-network-config в разделе «🔴 РЕШЕНО БЕЗ РЕБУТА: killall -9 dnsmasq». Скилл — процедурная память для будущих сессий, док — контекст.

Порядок применения на будущее: сначала скилл (что делать), потом док (почему и как устроено). При расхождении — верить фактам, снимать заново и править оба.