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

970 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <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 (если не стоит):
```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 `<rating>` тег
---
## Шаг 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)