Files
obsidian-vault/family/projects/media-toolbox-kraken.md
T

39 KiB
Raw Blame History

created, updated, status, tags
created updated status tags
2026-05-13 2026-05-13 planning
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 → если не нашёл → агент разбирается тут же

Реализация (примерно):

// 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.

Цель: найти реальную структуру медиабиблиотеки, где симлинки, куда ведут.

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

# С Mac или Кракена:
wakeonlan B0:6E:BF:60:6D:64
# или
python3 ~/Automation/wake-on-lan.py

Предотвратить сон во время SSH / копирования

Вариант A — systemd-inhibit (рекомендуется):

# Запустить на 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 (только экран):

# Только блокирует screensaver, не suspend
while true; do xdg-screensaver reset; sleep 60; done &

Рекомендация: вариант A (systemd-inhibit). Запускать в tmux, чтобы сессия жила независимо от SSH:

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 кладёт готовые

Создать директории

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 (прямо с Кракена)

# Запустить на Кракене (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:

# На 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.

Проверить конфиг:

cat /home/kraken/media/docker-compose.yml

Download dir должен быть смонтирован как HDD/media/downloads.

Порты: 9091 (RPC), 51413 (peers)

Если Transmission ещё не настроен — проверить и обновить docker-compose, добавить volume:

volumes:
  - /srv/dev-disk-by-uuid-.../media/downloads:/downloads
  - /srv/dev-disk-by-uuid-.../docker/transmission:/config

Шаг 5 — Перенос torrent-базы с HTPC

Найти торрент-клиент на HTPC

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:

# Скопировать на Кракен:
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 файлов:

# Скопировать с 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:

# 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 клиент

// 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 Идентификация медиа

// 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)

// 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 расширение

{
  "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 <specific_path>] [--fix-paths]
  --dry-run     Показать что будет сделано, не двигать файлы
  --path PATH   Обработать только конкретную папку/файл
  --fix-paths   Обновить пути в Transmission для уже существующих файлов

Шаг 7 — Cross-compilation Mac → Linux ARM64

Swift на Mac имеет встроенный cross-compiler для Linux.

Установить Swift toolchain для Linux (если не стоит):

# На Mac:
swiftly install latest
# или скачать Linux ARM toolchain:
# https://www.swift.org/install/linux/

Build для aarch64-linux:

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:

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 на Кракен:

# Сохранить образ и скопировать:
docker save kodi-media-sync:arm64 | gzip | ssh kraken@192.168.1.14 "docker load"

Шаг 8 — Docker-образ на Кракене

/home/kraken/media-sync/docker-compose.yml:

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

Запуск (разовый):

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 для Кракена

{
  "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 и ручная проверка

# 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.

Инструменты

// Поверх существующего 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 (с валидацией)

{
  "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
[
  {
    "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

# Разрезолвить конкретный файл вручную (из файла)
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:

#!/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 <rating> тег

Шаг 12 — Transmission download-complete hook

Transmission вызывает скрипт при завершении скачки:

# 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 (внутри контейнера):

#!/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 (рекомендуется как агрегатор):

# 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

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.aggregateCandidatesoriginalVariant выбирался по 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.ymldocker compose up -d)
  • Hermes cron на Кракене: scripts/kraken-agent-cron.sh 2x/day
  • Rutracker-скилл (шаг 14)