--- created: '2026-05-13' updated: '2026-05-13' status: planning tags: - kraken - media - jellyfin - transmission - swift - project --- # Media Toolbox на Кракене > Создано: 2026-05-13. Цель: полноценный медиасервер на Кракене (RPi 5) — перенос библиотеки с HTPC, автосортировка через kodi-media-sync, Jellyfin, торренты. --- ## Архитектура (целевая) ``` HTPC (Bazzite 192.168.1.86) └── /run/media/bazzite/Data/{Мультфильмы,Сериалы,movies,...} └─── 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 (сортировщик + метаданные) 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, скопировать БД торрентов) ``` --- ## Статус по шагам | Шаг | Статус | Заметки | |-----|--------|---------| | 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 | 🔲 | | --- ## Важные замечания по архитектуре ### Кеширование API-ответов ✅ `HTTPClient` кеширует все ответы в файловую систему (30-дневный TTL), включая при dry run. Кеш: `/srv/.../docker/media-pipeline/cache/`. Повторные resolve-прогоны не тратят API-лимиты. ### 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). ### Встроенный агент вместо Hermes cron ⭐ **Идея (под вопросом / для реализации):** вместо внешнего Hermes cron-джоба — встроить в `media-pipeline` интерактивный режим `resolve --interactive` (или отдельная команда `agent`). Концепция: - Для файлов которые не нашлись через TMDB/KP/webSearch — не просто записать "unresolved", а запустить mini-агентскую сессию - Агент имеет промпты + тулы (веб-поиск, TMDB по ID, Kinopoisk по ID, пользовательский ввод) - Может задавать уточняющие вопросы если нашёл несколько кандидатов - Сохраняет результат в кеш чтобы последующие прогоны не тратили API - Это делает media-pipeline самодостаточным — не зависит от внешнего оркестратора Преимущества 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/документальные/` (или аналог) --- ## Шаг 2 — Wake HTPC + предотвращение сна ### Wake on LAN ```bash # С Mac или Кракена: wakeonlan B0:6E:BF:60:6D:64 # или python3 ~/Automation/wake-on-lan.py ``` ### Предотвратить сон во время SSH / копирования **Вариант 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'" ``` --- ## Шаг 3 — Копирование медиа HTPC → Kraken ### Целевая структура на Кракене ``` /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/media/ ├── movies/ ├── cartoons/ ├── series/ ├── documentaries/ └── downloads/ ← Transmission download dir └── complete/ ← сюда Transmission кладёт готовые ``` ### Создать директории ```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" ``` > ⚠️ Скрипт должен быть доступен изнутри контейнера Transmission, или монтируется туда. > Альтернатива: Transmission вызывает скрипт на хосте → хост запускает docker run. --- ## Шаг 13 — Hermes cron на Кракене **Добавить через Hermes на Кракене:** ``` Крон: каждые 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/` --- ## Журнал выполнения ### 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)