From 0d2155717a1a225b7132136f35186bf3a1add596 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 13 May 2026 23:02:46 +0600 Subject: [PATCH] Failed to authenticate. API Error: 401 {"type":"error","error":{"type":"authentication_error","message":"Invalid authentication credentials"},"request_id":"req_011CazvpNUYZsiktTAQdjFo5"} [2026-05-13] vault sync --- family/projects/media-toolbox-kraken.md | 1006 +++-------------------- 1 file changed, 100 insertions(+), 906 deletions(-) diff --git a/family/projects/media-toolbox-kraken.md b/family/projects/media-toolbox-kraken.md index aa891c5b..265b5bbd 100644 --- a/family/projects/media-toolbox-kraken.md +++ b/family/projects/media-toolbox-kraken.md @@ -1,7 +1,7 @@ --- created: '2026-05-13' -updated: '2026-05-13' -status: planning +updated: '2026-05-14' +status: in-progress tags: - kraken - media @@ -12,7 +12,7 @@ tags: --- # Media Toolbox на Кракене -> Создано: 2026-05-13. Цель: полноценный медиасервер на Кракене (RPi 5) — перенос библиотеки с HTPC, автосортировка через kodi-media-sync, Jellyfin, торренты. +> Цель: полноценный медиасервер на Кракене (RPi 5) — перенос библиотеки с HTPC, автосортировка через media-pipeline (Swift), Jellyfin, торренты. --- @@ -20,950 +20,144 @@ tags: ``` HTPC (Bazzite 192.168.1.86) - └── /run/media/bazzite/Data/{Мультфильмы,Сериалы,movies,...} - └─── rsync ──→ Kraken HDD /srv/.../media/ + └── rsync → Kraken HDD /srv/.../media/ Kraken (RPi 5, 192.168.1.14) - ├── /srv/dev-disk-by-uuid-.../media/ - │ ├── movies/ ← фильмы - │ ├── cartoons/ ← мультфильмы - │ ├── series/ ← сериалы - │ ├── documentaries/ ← документальные - │ └── downloads/ ← папка Transmission (входящие) - ├── Docker: Transmission (скачка торрентов) - ├── Docker: Jellyfin (стриминг на TV/телефоны) - └── Docker: kodi-media-sync (сортировщик + метаданные) + ├── /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media/ + │ ├── movies/ ← фильмы (127 папок) + │ ├── cartoons/ ← мультфильмы (126 папок) + │ ├── series/ ← сериалы (45 папок) + │ ├── documentaries/ ← документальные (35 папок) + │ └── downloads/complete ← Transmission + ├── Docker: Transmission (128 торрентов, порт 9091) + ├── Jellyfin native (порт 8096) + └── Docker: media-pipeline:kraken (Swift CLI, arm64) -kodi-media-sync (Swift CLI, cross-compile Mac → Linux ARM64) - ├── Читает downloads/ + необработанные папки - ├── Идентифицирует через Transmission RPC (если торрент) или поиск по имени - ├── Создаёт NFO-файлы (Kodi/Jellyfin-совместимые) - ├── Раскладывает по категориям (movies/cartoons/series/docs) - ├── Обновляет пути скачки в Transmission через RPC API - └── Запускается как download-complete скрипт Transmission - -Hermes cron (Кракен, 1-2x/day) - └── kodi-media-sync --dry-run → лог → manual confirm / auto-apply - -TrueNAS (Taiga) - └── Transmission → миграция на Крaken (или оставить для seed, скопировать БД торрентов) +media-pipeline (~/Developer/media_files_db, ветка kraken-agent) + ├── scan → resolve → apply (sync) + ├── NFO-файлы для Jellyfin/Kodi + ├── Transmission RPC для обновления путей + └── unresolved.json — реестр нераспознанных ``` --- -## Статус по шагам +## Текущий статус (2026-05-14) -| Шаг | Статус | Заметки | -|-----|--------|---------| -| 1 — Исследование (Kodi lib на HTPC) | ✅ | Data: мультфильмы(229G) сериалы(116G) movies(673G) bbc(284G) | -| 2 — Wake HTPC + prevent sleep | ✅ | sleep masked, linger=yes, rsync через systemd-run --user | -| 3 — Копирование медиа HTPC → Kraken | 🔄 | ~58GB/категорию скопировано, ~1.3TB всего, rsync активен | -| 4 — Transmission на Кракене | ✅ | 128 торрентов (7 старых + 121 новый), paused, категоризированы | -| 5 — Перенос torrent-базы | ✅ | 121 медиа-торрент импортирован из Windows Downloads | -| 6 — MediaFilesScraper: изучить + настроить | ✅ | Swift 6 SPM, пропатчен для Linux, OpenRouter вместо OpenAI | -| 7 — Docker build ARM64 | ✅ | Colima 24GB, `--memory=20g -j 1`, образ 78MB задеплоен | -| 8 — Конфиг на Кракене | ✅ | config.json, on-download-complete.sh на месте | -| 9 — Dry run | 🔄 | 1/6 resolved (Black Mirror S06E01 ✅), остальное ждёт rsync | -| 10 — OpenRouter ключ | ⚠️ | Нужно вставить sk-or-v1-... в config.json на Кракене | -| 11 — Агент для unresolved | 🔲 | см. шаг 16 ниже | -| 12 — Jellyfin: настройка библиотек | 🔲 | после rsync | -| 13 — Transmission download-complete hook | ✅ | установлен и прописан | -| 14 — Hermes cron (unresolved notify) | 🔲 | см. шаг 16 | -| 15 — Rutracker-скилл | 🔲 | | -| 16 — Музыка: sync с TrueNAS | 🔲 | | +### Sync результаты (последний прогон) +| Категория | Папок | NFO | Статус | +|-----------|-------|-----|--------| +| movies | 127 | 127 | ✅ 100% | +| series | 45 | 238 (эпизоды) | ✅ хорошо | +| cartoons | 126 | ~30 | ⚠️ советские не резолвятся | +| documentaries | 35 | ~5 | ⚠️ | +| **Итого** | **317 regions** | **222 resolved** | **70%** | + +### Что работает +- ✅ Docker образ `media-pipeline:kraken` задеплоен (ветка `kraken-agent`) +- ✅ `sync` корректно вызывается (был баг: двойное `media-pipeline media-pipeline`) +- ✅ Кеш API-ответов: теперь `/config/cache` (persistent), 30-дневный TTL +- ✅ Фильмы: 127/127 NFO +- ✅ Сериалы: хорошо (Black Mirror, Queens Gambit, Good Omens и т.д.) +- ✅ Transmission download-complete hook установлен +- ✅ `resolve-manual` работает (форс-резолв по TMDb/KP ID) + +### Что не работает / в работе +- ⚠️ **Советские мультики** — `bremenskie.muzykanty`, `budilnik`, `ёжик в тумане` и ~50 других не резолвятся автоматически. Legacy Go-код справлялся — нужно разобраться в чём разница +- ⚠️ **Часть Disney/Pixar** в batch-режиме не проходят arbiter (Encanto, Lightyear, Inside Out) — одиночно через `resolve-manual` работают +- 🔲 Jellyfin библиотеки — не настроены +- 🔲 torrent-fix для 7 старых торрентов с путями `D:/Movies` +- 🔲 Hermes cron на Кракене --- -## Важные замечания по архитектуре +## Исправленные баги -### Кеширование API-ответов ✅ -`HTTPClient` кеширует все ответы в файловую систему (30-дневный TTL), включая при dry run. Кеш: `/srv/.../docker/media-pipeline/cache/`. Повторные resolve-прогоны не тратят API-лимиты. +### Баг 1: Двойной вызов binary (КРИТИЧЕСКИЙ) +**Проблема:** `docker run ... media-pipeline:kraken media-pipeline sync` — бинарь получал `media-pipeline` как первый аргумент = неизвестный subcommand → general help. Все прошлые dry-run не работали. +**Фикс:** `ENTRYPOINT ["media-pipeline"]` → правильный вызов `docker run ... sync ...` -### AI-агент: OpenRouter вместо OpenAI -`ChatGPTAPI` переключён на `https://openrouter.ai/api/v1/chat/completions`, модель по умолчанию `openai/gpt-4o-mini:free`. Ключ: `openai_api_key` в config.json — нужен реальный `sk-or-v1-...` ключ с openrouter.ai (бесплатный tier достаточен для Tier 5 last-resort). +### Баг 2: TitleSimilarity — симметричный Jaccard +**Проблема:** `encanto_2021_bdrip_by_dalemake` vs `Encanto`: `lengthRatio = 1/5 = 0.2` → confidence ≈ 0.05 → rejected. Noise-токены (bdrip, 1080p, год) убивали матч. +**Фикс:** Asymmetric scoring — когда query длиннее candidate, переключаемся на candidate-coverage Jaccard (покрытие кандидата запросом). `lengthRatio = 1.0` когда query длиннее. +**Коммит:** `2704ce3` -### Встроенный агент вместо Hermes cron ⭐ -**Идея (под вопросом / для реализации):** вместо внешнего Hermes cron-джоба — встроить в `media-pipeline` интерактивный режим `resolve --interactive` (или отдельная команда `agent`). +### Баг 3: ProviderSearchStrategy.maxCallsPerRun = 8 +**Проблема:** 8 API-вызовов на провайдер → 192 из 200 items никогда не искались. +**Фикс:** 8 → 500. Коммит `2704ce3` -Концепция: -- Для файлов которые не нашлись через TMDB/KP/webSearch — не просто записать "unresolved", а запустить mini-агентскую сессию -- Агент имеет промпты + тулы (веб-поиск, TMDB по ID, Kinopoisk по ID, пользовательский ввод) -- Может задавать уточняющие вопросы если нашёл несколько кандидатов -- Сохраняет результат в кеш чтобы последующие прогоны не тратили API -- Это делает media-pipeline самодостаточным — не зависит от внешнего оркестратора +### Баг 4: Series-гипотезы для одиночных файлов +**Проблема:** `NumberedEpisodesHypothesisStrategy` и `NamedEpisodesHypothesisStrategy` генерировали series-гипотезы для одиночных `.avi`/`.mkv` без S/E маркеров → TMDb искал по `/search/tv` вместо `/search/movie`. +**Фикс:** `videos.count < 2 && !hasAnyEpisodeMarkers → return nil`. Коммит `17c2ddc` -Преимущества vs Hermes cron: -- Нет latency между обнаружением проблемы и её решением -- Контекст resolve-сессии доступен агенту напрямую (graph facts, провайдеры, история) -- Работает как download-complete хук: скачал → resolve → если не нашёл → агент разбирается тут же - -Реализация (примерно): -```swift -// media-pipeline agent --config /config/config.json [--path ...] -// Запускает resolve, для unresolved — интерактивная LLM-сессия с тулами -struct AgentCommand { - // Инструменты: - // - tmdb_search(query, year) → candidates[] - // - kp_search(query) → candidates[] - // - web_search(query) → snippets[] - // - confirm(candidate) → принять результат - // - skip() → пометить как manual-review-needed -} -``` - -**Статус**: под вопросом — оценить объём работ перед реализацией. Возможно проще оставить Hermes cron как оркестратор но дать ему доступ к graph facts через `--format json`. - -**Цель:** найти реальную структуру медиабиблиотеки, где симлинки, куда ведут. - -```bash -ssh -i ~/.ssh/id_rsa bazzite@192.168.1.86 - -# Kodi userdata (может быть в двух местах) -ls ~/.kodi/userdata/ -ls ~/.kodi/userdata/sources.xml # список медиаисточников - -# Если Kodi с Windows раздела мигрировал -ls /run/media/bazzite/Data/AppData/Roaming/Kodi/ 2>/dev/null - -# Найти симлинки в home -find ~ -type l 2>/dev/null | head -30 - -# Медиа на Data разделе -ls /run/media/bazzite/Data/ | grep -iE "film|movie|кино|мульт|serial|сери|doc" -``` - -**Ожидаемые пути на Data:** -- `/run/media/bazzite/Data/Мультфильмы/` -- `/run/media/bazzite/Data/Сериалы/` -- `/run/media/bazzite/Data/movies/` -- `/run/media/bazzite/Data/документальные/` (или аналог) +### Баг 5: HTTP кеш не персистился +**Проблема:** `cacheDirectory = /usr/local/bin/cache` внутри контейнера. `docker run --rm` → кеш удалялся при каждом запуске → тысячи дублирующих API запросов. +**Фикс:** Autodetect `/config` mount → `/config/cache` (persistent volume). Коммит `5a1053c` --- -## Шаг 2 — Wake HTPC + предотвращение сна +## Следующий шаг: советские мультики -### Wake on LAN -```bash -# С Mac или Кракена: -wakeonlan B0:6E:BF:60:6D:64 -# или -python3 ~/Automation/wake-on-lan.py -``` +**Задача:** понять почему `bremenskie.muzykanty`, `budilnik 1984`, `ёжик в тумане` и ~50 других советских мультиков не резолвятся, хотя легаси Go-код справлялся. -### Предотвратить сон во время SSH / копирования +**Гипотезы:** +1. Транслитерация → KP не ищет по Latin `bremenskie` → нужна Cyrillic варианты в query +2. NameTokenization не генерирует качественную обратную транслит для советских названий +3. KP API возвращает результаты, но TitleSimilarity не матчит (разные скрипты) +4. TMDb `dual` search по `bremenskie muzykanty` → нет результатов → нужен только KP -**Вариант A — systemd-inhibit (рекомендуется):** -```bash -# Запустить на HTPC в отдельной SSH-сессии, оставить висеть пока идёт копирование: -systemd-inhibit --what=sleep:idle --who="media-copy" \ - --why="Copying media to Kraken" --mode=block \ - sleep infinity -``` -Закрыть эту сессию = сон разрешён снова. - -**Вариант B — через systemd сервис-lockfile (долгосрочно):** -Если SSH-сессия падает, нужен более надёжный способ. Создать временный `inhibit.service` который поднимается на время rsync. - -**Вариант C — xdg-screensaver (только экран):** -```bash -# Только блокирует screensaver, не suspend -while true; do xdg-screensaver reset; sleep 60; done & -``` - -**Рекомендация:** вариант A (systemd-inhibit). Запускать в tmux, чтобы сессия жила независимо от SSH: -```bash -ssh bazzite@192.168.1.86 "tmux new-session -d -s inhibit 'systemd-inhibit --what=sleep:idle --who=media-copy --why=Copying --mode=block sleep 86400'" -``` +**Что делать:** +- Проверить что именно возвращает KP для `bremenskie muzykanty` +- Проверить что variant'ы генерирует NameTokenization для этих файлов +- Если проблема в transliteration accuracy — починить в TitleSimilarity (cross-script fuzzy match) --- -## Шаг 3 — Копирование медиа HTPC → Kraken +## Конфиг на Кракене -### Целевая структура на Кракене ``` -/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media/ -├── movies/ -├── cartoons/ -├── series/ -├── documentaries/ -└── downloads/ ← Transmission download dir - └── complete/ ← сюда Transmission кладёт готовые +/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/media-pipeline/ +├── config.json ← TMDb/KP ключи, Transmission RPC, directories +├── unresolved.json ← реестр нераспознанных (создаётся автоматически) +├── cache/ ← HTTP кеш 30 дней (после фикса) +└── on-download-complete.sh ``` -### Создать директории +**Запуск sync:** ```bash -ssh kraken@192.168.1.14 "mkdir -p /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media/{movies,cartoons,series,documentaries,downloads/complete,downloads/incomplete}" -``` - -### rsync HTPC → Kraken (прямо с Кракена) -```bash -# Запустить на Кракене (pull с HTPC): -HDD=/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade - -# Мультфильмы -rsync -avh --progress --no-links \ - bazzite@192.168.1.86:"/run/media/bazzite/Data/Мультфильмы/" \ - "$HDD/media/cartoons/" - -# Сериалы -rsync -avh --progress --no-links \ - bazzite@192.168.1.86:"/run/media/bazzite/Data/Сериалы/" \ - "$HDD/media/series/" - -# Фильмы (уточнить точное имя папки) -rsync -avh --progress --no-links \ - bazzite@192.168.1.86:"/run/media/bazzite/Data/movies/" \ - "$HDD/media/movies/" - -# Документальные (если есть) -rsync -avh --progress --no-links \ - bazzite@192.168.1.86:"/run/media/bazzite/Data/документальные/" \ - "$HDD/media/documentaries/" 2>/dev/null || true -``` - -> ⚠️ `--no-links` — не копировать симлинки как симлинки, следовать им. -> ⚠️ Для SSH без пароля с Кракена нужен ключ: `ssh-copy-id -i ~/.ssh/id_rsa bazzite@192.168.1.86` - -### Альтернатива — пушить с HTPC через SSH: -```bash -# На HTPC: -rsync -avh --progress /run/media/bazzite/Data/Мультфильмы/ \ - kraken@192.168.1.14:/srv/.../media/cartoons/ -``` - ---- - -## Шаг 4 — Transmission на Кракене - -Transmission уже задеплоен вместе с Jellyfin в `/home/kraken/media/docker-compose.yml`. - -**Проверить конфиг:** -```bash -cat /home/kraken/media/docker-compose.yml -``` - -**Download dir** должен быть смонтирован как HDD/media/downloads. - -**Порты:** 9091 (RPC), 51413 (peers) - -**Если Transmission ещё не настроен** — проверить и обновить docker-compose, добавить volume: -```yaml -volumes: - - /srv/dev-disk-by-uuid-.../media/downloads:/downloads - - /srv/dev-disk-by-uuid-.../docker/transmission:/config -``` - ---- - -## Шаг 5 — Перенос torrent-базы с HTPC - -### Найти торрент-клиент на HTPC - -```bash -ssh bazzite@192.168.1.86 - -# Что стоит? -which transmission-daemon qbittorrent-nox rtorrent 2>/dev/null -systemctl list-units | grep -iE "torrent|transmission|qbit" - -# Transmission (если есть): -ls ~/.config/transmission/torrents/ | head -10 -ls ~/.config/transmission/resume/ | head -10 - -# qBittorrent: -ls ~/.local/share/data/qBittorrent/BT_backup/ | head -10 -``` - -### Если Transmission на Bazzite: -```bash -# Скопировать на Кракен: -rsync -avh bazzite@192.168.1.86:~/.config/transmission/ \ - kraken@192.168.1.14:/tmp/htpc-transmission/ -``` - -### Если торрент-клиент был на Windows (TrueNAS Transmission): -Торрентная БД TrueNAS уже работает. Нужно решить: -- **Вариант A:** Перенести TrueNAS Transmission на Крaken (один клиент) -- **Вариант B:** Оставить TrueNAS для seed, Крaken для новых загрузок - -**Миграция `.torrent` + `.resume` файлов:** -```bash -# Скопировать с TrueNAS (через duckdns): -rsync -avh -e "ssh -i ~/.ssh/id_rsa" \ - truenas_admin@mallexxx.duckdns.org:/mnt/RED_2TB/docker/transmission/ \ - /tmp/taiga-transmission/ - -# Поправить пути в .resume файлах: -# Старый путь TrueNAS: /mnt/storage/Downloads/... -# Новый путь Кракена: /downloads/complete/... -for f in /tmp/taiga-transmission/resume/*.resume; do - # .resume это bencoded файл — нужен python с bencode или transmission-edit - echo "$f" -done -``` - -> ⚠️ Файлы `.resume` — бинарный bencode-формат. Для правки путей использовать: -> `pip install bencoder.pyx` или `transmission-remote --torrent N --find /new/path` -> Проще: после импорта торрентов запустить media-sync с --fix-paths флагом через RPC API. - -### Исправление путей через Transmission RPC: -```bash -# Transmission RPC: изменить путь для конкретного торрента -curl -s -u user:pass http://kraken:9091/transmission/rpc \ - -H "X-Transmission-Session-Id: TOKEN" \ - -d '{"method":"torrent-set-location","arguments":{"ids":[N],"location":"/downloads/complete/FolderName","move":false}}' -``` - -**kodi-media-sync должен уметь это автоматически** (см. шаг 6). - ---- - -## Шаг 6 — kodi-media-sync: функциональность - -### Текущее состояние проекта -Проект `~/Developer/kodi-media-sync` (Swift): -- `Config.swift` — читает config.json: `transmission` URL, `directories[]`, `output.{movies,series}` -- `MediaSync.swift` — scaffold: сканирует директории, вызывает `updateMetadata` (пустая) -- `Path.swift` — утилиты для файловой системы - -**Нужно реализовать:** - -### 6.1 Transmission RPC клиент -```swift -// Sources/TransmissionClient.swift -struct TransmissionClient { - let url: URL - - // Получить все торренты - func getTorrents() async throws -> [Torrent] - - // Найти торрент по имени файла / папке - func findTorrent(forPath path: Path) async throws -> Torrent? - - // Обновить путь торрента - func setLocation(torrentId: Int, location: Path) async throws -} -``` - -### 6.2 Идентификация медиа -```swift -// Sources/MediaIdentifier.swift -enum MediaType { - case movie(title: String, year: Int?) - case series(title: String, season: Int?, episode: Int?) - case cartoon(title: String, year: Int?) - case documentary(title: String, year: Int?) - case unknown -} - -struct MediaIdentifier { - // Из имени папки/файла → тип медиа + нормализованное название - func identify(path: Path, torrent: Torrent?) -> MediaType - - // Поиск метаданных (TMDB/OMDB/локальный поиск) - func fetchMetadata(for media: MediaType) async throws -> MediaMetadata -} -``` - -**Алгоритм идентификации:** -1. Если есть торрент в Transmission — взять имя торрента, категорию -2. Парсить имя файла/папки: регексы для `S01E01`, год `(2024)`, качество `1080p` -3. Для документалок типа "знакомимся с кабанами" — поиск по названию (TMDB API) -4. Маппинг исходной папки → тип: `Мультфильмы/` → cartoon, `Сериалы/` → series - -### 6.3 NFO-файлы (Kodi/Jellyfin) -```swift -// Sources/NFOWriter.swift -// Формат Kodi NFO: https://kodi.wiki/view/NFO_files/Movies -struct NFOWriter { - func writeMovieNFO(_ metadata: MediaMetadata, to path: Path) throws - func writeSeriesNFO(_ metadata: MediaMetadata, to path: Path) throws - func writeTVShowNFO(_ metadata: MediaMetadata, to path: Path) throws -} -``` - -### 6.4 Config расширение -```json -{ - "transmission": "http://user:pass@192.168.1.14:9091/transmission/rpc", - "tmdb_api_key": "...", - "directories": [ - "/media/downloads/complete", - "/media/movies", - "/media/cartoons" - ], - "output": { - "movies": "/media/movies", - "series": "/media/series", - "cartoons": "/media/cartoons", - "documentaries": "/media/documentaries" - }, - "dry_run": false, - "category_hints": { - "/media/cartoons": "cartoon", - "/media/series": "series", - "/media/documentaries": "documentary" - } -} -``` - -### 6.5 CLI флаги -``` -kodi-media-sync [--config path] [--dry-run] [--path ] [--fix-paths] - --dry-run Показать что будет сделано, не двигать файлы - --path PATH Обработать только конкретную папку/файл - --fix-paths Обновить пути в Transmission для уже существующих файлов -``` - ---- - -## Шаг 7 — Cross-compilation Mac → Linux ARM64 - -**Swift на Mac имеет встроенный cross-compiler для Linux.** - -### Установить Swift toolchain для Linux (если не стоит): -```bash -# На Mac: -swiftly install latest -# или скачать Linux ARM toolchain: -# https://www.swift.org/install/linux/ -``` - -### Build для aarch64-linux: -```bash -cd ~/Developer/kodi-media-sync - -# Вариант A — через Docker buildx (рекомендуется): -docker buildx build --platform linux/arm64 \ - -f Dockerfile.linux \ - -t kodi-media-sync:arm64 \ - --output type=local,dest=./build-arm64 . - -# Вариант B — swift cross-compile (нужен SDK): -swift build --triple aarch64-unknown-linux-gnu \ - --sdk /path/to/linux-arm64-sdk -``` - -### Dockerfile.linux для Крaken: -```dockerfile -FROM swift:5.9-jammy AS builder -WORKDIR /build -COPY . . -RUN swift build -c release - -FROM ubuntu:22.04 -RUN apt-get update && apt-get install -y libstdc++6 libgcc-s1 && rm -rf /var/lib/apt/lists/* -COPY --from=builder /build/.build/release/kodi-media-sync /usr/local/bin/ -COPY config.json /etc/kodi-media-sync/config.json -ENTRYPOINT ["kodi-media-sync"] -CMD ["--config", "/etc/kodi-media-sync/config.json"] -``` - -> ⚠️ Собирать образ на Mac через buildx с эмуляцией QEMU — не на RPi5 (undervoltage под нагрузкой)! - -### Deploy на Кракен: -```bash -# Сохранить образ и скопировать: -docker save kodi-media-sync:arm64 | gzip | ssh kraken@192.168.1.14 "docker load" -``` - ---- - -## Шаг 8 — Docker-образ на Кракене - -### `/home/kraken/media-sync/docker-compose.yml`: -```yaml -version: '3.8' -services: - kodi-media-sync: - image: kodi-media-sync:arm64 - container_name: kodi-media-sync - restart: "no" # запускается по требованию, не автостарт - volumes: - - /srv/dev-disk-by-uuid-.../media:/media - - /srv/dev-disk-by-uuid-.../docker/media-sync:/config - environment: - - CONFIG=/config/config.json - network_mode: host # доступ к Transmission на localhost -``` - -### Запуск (разовый): -```bash -cd /home/kraken/media-sync -sudo docker compose run --rm kodi-media-sync --dry-run -sudo docker compose run --rm kodi-media-sync -``` - ---- - -## Шаг 9 — Config для Кракена - -```json -{ - "transmission": "http://admin:pass@192.168.1.14:9091/transmission/rpc", - "tmdb_api_key": "КЛЮЧ_ИЗ_TMDB", - "directories": [ - "/media/downloads/complete" - ], - "category_hints": { - "/media/cartoons": "cartoon", - "/media/series": "series", - "/media/documentaries": "documentary", - "/media/movies": "movie" - }, - "output": { - "movies": "/media/movies", - "series": "/media/series", - "cartoons": "/media/cartoons", - "documentaries": "/media/documentaries" - }, - "dry_run": false -} -``` - -> TMDB API key: получить на https://www.themoviedb.org/settings/api (бесплатно) - ---- - -## Шаг 10 — Dry run и ручная проверка - -```bash -# Dry run — показывает что будет сделано -sudo docker run --rm \ - -v /srv/.../media:/media \ - -v /srv/.../docker/media-sync:/config \ - kodi-media-sync:arm64 --dry-run - -# Проверить проблемные документалки -sudo docker run --rm ... kodi-media-sync:arm64 --dry-run \ - --path "/media/downloads/complete/знакомимся с кабанами" - -# Проверить NFO файлы -cat /srv/.../media/movies/SomeMovie/SomeMovie.nfo -``` - -**Что проверить:** -- [ ] Фильмы попали в movies/, мультфильмы в cartoons/ и т.д. -- [ ] NFO-файлы созданы и содержат правильные title/year/plot -- [ ] Документалки с нестандартными названиями найдены через TMDB -- [ ] Торренты в Transmission показывают правильные пути - ---- - -## Шаг 11 (новый) — Встроенный агент для unresolved файлов - -### Концепция - -Когда стандартный resolve-пайплайн (TMDB → KP → IMDb → WebSearch) не смог идентифицировать файл, вместо записи "unresolved" — запускается LLM-агент с инструментами. Агент работает как последний уровень: пробует поиск по-другому, подтверждает кандидата, либо честно записывает "не могу". - -**Активация:** `media-pipeline agent --config /config/config.json [--path ...]` -Или автоматически после `resolve` если остались unresolved регионы. - ---- - -### Архитектура агента - -#### Промпт (системный) -``` -You are a media file identifier. You help identify what a file is so it can be -categorized correctly in a media library. - -You are given: -- File path and name -- If the file is NOT in a root media folder: full listing of its parent directory - (helps distinguish: single movie, series season pack, documentary collection, etc.) -- Any context already gathered (partial TMDB/KP results, failed queries) - -Your job: identify the content and return a structured JSON result. - -Tools available: -- search(query) → DuckDuckGo HTML results (titles, URLs, snippets) -- fetch(url) → raw page content (use for TMDB/IMDb/KP specific pages) - -Files can be: movie, series, cartoon, documentary, music album, game, book, -software, or other. Support "uncategorizable" as last resort. - -Always prefer TMDB ID when available. Think step by step. Use tools to verify. -``` - -#### Инструменты - -```swift -// Поверх существующего DuckDuckGo HTML scraper (уже есть в IMDbAPI/WebSearchStrategy) -struct AgentSearchTool { - func search(_ query: String) async -> [SearchResult] // DDG HTML, top 5 - func fetch(_ url: URL) async -> String // curl-like, обрезан до 8k символов -} -``` - -#### Выходной JSON (с валидацией) - -```json -{ - "status": "resolved" | "uncategorizable" | "needs_manual", - - // Если resolved: - "media_type": "movie" | "series" | "cartoon" | "documentary" | - "music" | "game" | "book" | "software" | "other", - "title": "Название на языке оригинала", - "title_ru": "Русское название (если известно)", - "year": 2024, - - // IDs (хотя бы один обязателен для resolved): - "tmdb_id": "12345", - "imdb_id": "tt1234567", - "kp_id": "12345", - - // Для сериалов: - "season": 1, - "episode": 5, // если отдельный эпизод - "is_season_pack": true, - "episodes_count": 10, - - // Метаданные: - "genres": ["драма", "триллер"], - "confidence": 0.9, // 0.0–1.0, агент сам оценивает - "notes": "...", // почему uncategorizable или что смущает - - // Для группировки: - "is_sideband": false, // субтитры, NFO, постер — не самостоятельный контент - "parent_title": "...", // если sideband — к чему относится -} -``` - -**Валидация:** после каждого ответа LLM — JSON schema check. Если невалидный — повторный запрос с описанием ошибки (до 2 попыток исправить). - ---- - -### Хранилище unresolved файлов - -``` -/srv/.../docker/media-pipeline/unresolved.json -``` - -```json -[ - { - "path": "/media/downloads/complete/Знакомимся с кабанами", - "added_at": "2026-05-13T04:00:00Z", - "last_attempt": "2026-05-13T04:00:00Z", - "attempts": 1, - "agent_notes": "Не нашлось в TMDB/KP, агент тоже не смог — слишком общее название", - "status": "needs_manual" - } -] -``` - ---- - -### Force-resolve через CLI - -```bash -# Разрезолвить конкретный файл вручную (из файла) -media-pipeline resolve-manual \ - --config /config/config.json \ - --path "/media/downloads/complete/Знакомимся с кабанами" \ - --result '{"status":"resolved","media_type":"documentary","title":"Meet the Boars","tmdb_id":"12345",...}' - -# Или из файла (удобно для скилла Кракена): -media-pipeline resolve-manual \ - --config /config/config.json \ - --from-file /tmp/manual-resolve.json -``` - -Что делает `resolve-manual`: -1. Берёт готовый JSON результат -2. Применяет (создаёт NFO, перемещает если нужно, обновляет Transmission пути) -3. Удаляет запись из `unresolved.json` -4. Пишет в лог - ---- - -### Cron: почасовой обход + Hermes notify - -**Скрипт-чекер** `/srv/.../docker/media-pipeline/check-unresolved.sh`: -```bash -#!/bin/bash -# Запускает agent только если есть новые unresolved файлы -UNRESOLVED=/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/media-pipeline/unresolved.json - -# Нет файла — нечего делать -[ -f "$UNRESOLVED" ] || exit 0 - -# Считаем needs_manual записи -COUNT=$(jq '[.[] | select(.status == "needs_manual")] | length' "$UNRESOLVED" 2>/dev/null || echo 0) - -# Нет needs_manual — тихо выходим (Hermes не получит пустое сообщение) -[ "$COUNT" -gt 0 ] || exit 0 - -echo "UNRESOLVED_COUNT=$COUNT" -# Вывод идёт в Hermes cron prompt как контекст -jq '[.[] | select(.status == "needs_manual") | {path, added_at, agent_notes}]' "$UNRESOLVED" -``` - -**Hermes cron** (каждый час, Кракен): -``` -Schedule: 0 * * * * -Script: /srv/.../docker/media-pipeline/check-unresolved.sh -Prompt: - Скрипт вернул список нераспознанных медиафайлов (вывод выше). - Для каждого файла: - 1. Посмотри на путь и agent_notes - 2. Если можешь идентифицировать (поиск, здравый смысл) — вызови: - docker run --rm ... media-pipeline resolve-manual --path "..." --result '{...}' - 3. Если не можешь — отправь уведомление в Telegram с описанием файла и попроси - меня разрезолвить вручную. - Не присылай сообщение если всё уже разрезолвлено или список пустой. -``` - -Скрипт запускается **до** обращения к модели — если пустой вывод, Hermes не получает запрос. Нет пустых сообщений. - ---- - -### Реализация в media_files_db - -**Новые файлы:** -``` -Sources/MediaPipeline/Agent/ - AgentCommand.swift ← CLICommand: media-pipeline agent - AgentSession.swift ← LLM loop: prompt → tools → validate → save - AgentTools.swift ← search() + fetch() wrappers - AgentResultSchema.swift ← AgentResult struct + JSON validation - UnresolvedStore.swift ← чтение/запись unresolved.json - -Sources/MediaPipeline/ResolveManual/ - ResolveManualCommand.swift ← media-pipeline resolve-manual -``` - -**Зависимости уже есть:** -- `ChatGPTAPI` — LLM клиент (переключён на OpenRouter) -- `IMDbAPI` / DDG HTML scraper — search tool -- `HTTPClient` — fetch tool -- Кеш — все LLM вызовы кешируются через `ChatGPTAPI.sendPrompt(cacheKey:)` - -**Оценка объёма:** ~400-500 строк Swift, 2-3 дня работы. - ---- - -Jellyfin уже работает на Кракене: http://192.168.1.14:8096 - -### Категории библиотек: -| Библиотека | Тип | Путь | Язык | -|------------|-----|------|------| -| Детские мультфильмы | Animated Movies | /media/cartoons | ru | -| Детские сериалы | Series | /media/cartoons-series | ru | -| Документальные | Movies | /media/documentaries | ru | -| Сериалы | TV Shows | /media/series | ru | -| Фильмы | Movies | /media/movies | ru | - -### Настройки библиотеки: -- **Metadata Language:** Russian (или auto-detect) -- **NFO reading:** включить (Kodi-compatible NFO) -- **Sort by:** Date Added (поле из NFO), Rating, Title - -### Сортировка и фильтрация в UI (Jellyfin): -- Детские: отдельная коллекция "Детям" → мультфильмы + детские сериалы -- Можно использовать **Collections** для кастомных подборок -- **Recently Added** — показывает по дате скачки (metadata: dateadded в NFO) -- Рейтинг: IMDB/TMDB — парсится из NFO `` тег - ---- - -## Шаг 12 — Transmission download-complete hook - -Transmission вызывает скрипт при завершении скачки: - -```bash -# settings.json Transmission (в /config/): -# "script-torrent-done-enabled": true, -# "script-torrent-done-filename": "/usr/local/bin/on-download-complete.sh" -``` - -### `/usr/local/bin/on-download-complete.sh` (внутри контейнера): -```bash -#!/bin/bash -# Transmission передаёт переменные: -# TR_TORRENT_NAME, TR_TORRENT_DIR, TR_TORRENT_ID, TR_APP_VERSION - -logger "Transmission: download complete: $TR_TORRENT_NAME" - -# Запустить media-sync для этой конкретной папки docker run --rm \ -v /srv/.../media:/media \ - -v /srv/.../docker/media-sync:/config \ - kodi-media-sync:arm64 \ - --path "$TR_TORRENT_DIR/$TR_TORRENT_NAME" + -v /srv/.../docker/media-pipeline:/config \ + media-pipeline:kraken \ + sync --config /config/config.json ``` -> ⚠️ Скрипт должен быть доступен изнутри контейнера Transmission, или монтируется туда. -> Альтернатива: Transmission вызывает скрипт на хосте → хост запускает docker run. - ---- - -## Шаг 13 — Hermes cron на Кракене - -**Добавить через Hermes на Кракене:** +**Force-resolve одного item:** +```bash +docker run --rm ... media-pipeline:kraken \ + resolve-manual --config /config/config.json \ + --media-path '/media/cartoons/bremenskie.muzykanty' \ + --tmdb 14327 ``` -Крон: каждые 12 часов -Задача: запустить kodi-media-sync --dry-run на папке downloads, прислать отчёт в Telegram -``` - -### Cron job (добавить в Hermes Кракена): -``` -Schedule: 0 */12 * * * -Prompt: Запусти kodi-media-sync на Кракене: - sudo docker run --rm \ - -v /srv/.../media:/media \ - -v /srv/.../docker/media-sync:/config \ - kodi-media-sync:arm64 --dry-run - Пришли отчёт: что было распознано, что нет. -``` - ---- - -## Шаг 14 — Rutracker / торрент-поиск скилл - -**Добавить скилл Кракену:** -- Поиск по Rutracker (может требовать авторизацию) -- Альтернативы: Jackett (агрегатор) + Prowlarr -- Добавление торрента в Transmission через RPC API - -**Jackett** (рекомендуется как агрегатор): -```yaml -# docker-compose для Jackett: -services: - jackett: - image: linuxserver/jackett - ports: - - "9117:9117" - volumes: - - /srv/.../docker/jackett:/config -``` -Jackett поддерживает Rutracker + сотни других tracker-ов, отдаёт unified API. - ---- - -## Шаг 15 — Музыка: скачка + sync с TrueNAS - -### Структура: -``` -Kraken: /media/music/ - ├── [Artist]/ - │ └── [Album]/ - │ └── *.mp3 - -TrueNAS: /mnt/RED_2TB/storage/Music/ (master copy) -``` - -### kodi-media-sync — расширение для музыки: -- Идентификация: MusicBrainz / Beets -- Тегирование через `beets` (python tool) -- Sync Kraken → TrueNAS через rsync - -### Jellyfin музыкальная библиотека: -- Тип: Music -- Путь: /media/music -- Плеер: Jellyfin + Finamp на Android --- ## Технические детали -### HTPC -- IP: 192.168.1.86 -- SSH: `ssh -i ~/.ssh/id_rsa bazzite@192.168.1.86` -- Data mount: `/run/media/bazzite/Data/` -- WoL MAC: `B0:6E:BF:60:6D:64` - -### Крaken -- IP: 192.168.1.14 -- SSH: `ssh -i ~/.ssh/id_rsa kraken@192.168.1.14` -- HDD: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/` -- Jellyfin: http://192.168.1.14:8096 -- Transmission: http://192.168.1.14:9091 - -### kodi-media-sync -- Исходники: `~/Developer/kodi-media-sync/` (Mac) -- Язык: Swift 5.9+ -- Зависимости: swift-argument-parser -- Target: Linux ARM64 (aarch64) - -### TrueNAS (Taiga) -- SSH: `truenas_admin@mallexxx.duckdns.org` -- Transmission config: `/mnt/RED_2TB/docker/transmission/` -- Downloads: `/mnt/RED_2TB/storage/Downloads/` +| | | +|--|--| +| HTPC | `bazzite@192.168.1.86`, Data: `/run/media/bazzite/Data/`, WoL: `B0:6E:BF:60:6D:64` | +| Кракен | `kraken@192.168.1.14`, HDD: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/` | +| Jellyfin | http://192.168.1.14:8096 | +| Transmission | http://192.168.1.14:9091 | +| Исходники | `~/Developer/media_files_db/`, ветка `kraken-agent` | +| Docker образ | `media-pipeline:kraken` (arm64, Colima 24GB) | --- -## Журнал выполнения +## Журнал -### 2026-05-13 — Активная работа -- Разбужен HTPC, сон заблокирован: `systemctl mask` sleep/suspend + `loginctl enable-linger bazzite` -- 4x rsync запущены через `systemd-run --user` (выживают без SSH): ~1.3TB -- Torrent-база Windows найдена (7 торрентов, `D:/Movies`), скопирована в `/tmp/transmission-backup/` -- Изучен MediaFilesScraper: полный Swift 6.0 SPM, бинарь `media-pipeline`, команды scan/resolve/apply/sync -- Пропатчен AVFoundationMediaMetadataReader для Linux (`#if canImport(AVFoundation)` + stub) -- `Package.swift`: платформа понижена до macOS(.v14) для совместимости -- `Dockerfile.kraken` + `docker-compose.kraken.yml` написаны -- `config.kraken.json` готов: Transmission RPC, TMDB/KP, все провайдеры, `dry_run=true` -- macOS build запущен в фоне для проверки компиляции - -### 2026-05-13 — Сессия 2 (Docker build + resolve) - -**Docker build `media-pipeline:kraken`** -- `.dockerignore` добавлен — исключает `.build/` (6.7GB), OOM устранён -- Colima: 24GB RAM, 8 CPU; `--memory=20g -j 1` -- Linux-фиксы: `#if os(Linux)` для CF-encoding (RutrackerAPI), `FoundationNetworking` в 3 файлах -- Образ `media-pipeline:kraken` (78MB) собран и задеплоен на Кракен ✅ - -**Resolve dry run (первый)** -- 6 region'ов; **1 resolved**: Black Mirror S06E01 → `seriesEpisodesNumbered`, support=0.90 ✅ -- Проблема: `ProviderSearchStrategy.aggregateCandidates` — `originalVariant` выбирался по non-ASCII (кириллица), которая давала 0 результатов от TMDB -- **Фикс**: `originalVariant` → первый вариант с реальными результатами; high-confidence (≥0.85) single-variant принимается без unique-result требования -- Все 6 ProviderSearchStrategy тестов прошли, образ пересобран и задеплоен -- `on-download-complete.sh` установлен, прописан в Transmission settings ✅ - -**Текущее состояние** -- rsync идёт (~19-20GB из сотен GB по категориям) -- Следующий dry run — когда rsync скопирует больше файлов с правильной структурой подпапок - -### Сессия 3 — 2026-05-13 (agent + deploy scripts) - -**Реализованы новые компоненты (`kraken-agent` ветка, коммиты 22abd5a, 4c91ddd):** - -**`UnresolvedEntry` / `UnresolvedStore`** — персистентный JSON-реестр нераспознанных регионов: -- Автоматически обновляется после каждого `resolve` / `sync` -- Поддерживает merge (upsert + remove resolved), atomic write -- Хранит: path, reason, queryNames, attemptCount, agentHint -- Файл: `/config/unresolved.json` (рядом с config.json) - -**`media-pipeline resolve-manual`** — force-resolve по ID: -``` -media-pipeline resolve-manual --media-path /media/movies/Сталкер \ - --tmdb 11 --type movie -``` -- TMDb / IMDb / Kinopoisk lookup → NFO write → удаление из unresolved.json - -**`media-pipeline agent`** — LLM-assisted batch: -``` -media-pipeline agent --apply --limit 30 -``` -- Pre-check: TMDb text search (score ≥ 0.85) → resolve без LLM -- Fallback: OpenRouter/GPT-4o-mini → corrected title → TMDb lookup → NFO -- Без `--apply`: сохраняет hints в unresolved.json для ревью - -**Deploy scripts:** -- `scripts/kraken-agent-cron.sh` — запуск агента 2x/день, pre-check на пустой unresolved.json -- `scripts/on-download-complete.sh` — download-complete hook: sync → torrent-set-location -- `scripts/transmission-location-update.sh` — RPC helper -- `deploy/docker-compose.jellyfin.yml` — Jellyfin + 5 библиотек + RPi5 V4L2 transcode - -### Что осталось -- [ ] Дождаться SSH на Кракен + вставить OpenRouter ключ в config.json -- [ ] Docker build нового образа (собирается через Colima, proc_5bfba754244a) -- [ ] Задеплоить новый образ на Кракен (`docker save | scp | docker load`) -- [ ] Дождаться rsync (~1.3TB), прогнать полный dry run -- [ ] torrent-set-location для 7 перенесённых торрентов (после rsync) -- [ ] Настроить Jellyfin (`deploy/docker-compose.jellyfin.yml` → `docker compose up -d`) -- [ ] Hermes cron на Кракене: `scripts/kraken-agent-cron.sh` 2x/day -- [ ] Rutracker-скилл (шаг 14) +- **2026-05-13 сессия 1:** HTPC разбужен, rsync запущен (~1.3TB), торрент-база импортирована (128 торрентов) +- **2026-05-13 сессия 2:** Docker build arm64, deploy на Кракен, first sync +- **2026-05-13 сессия 3:** `kraken-agent` ветка: UnresolvedStore, resolve-manual, agent, torrent-fix subcommands +- **2026-05-14 сессия 4:** Найдены и исправлены 5 системных багов (см. выше). Sync: 222/317 resolved. Кеш починен.