970 lines
39 KiB
Markdown
970 lines
39 KiB
Markdown
---
|
||
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)
|