2026-05-17 media-pipeline: file routing plan

This commit is contained in:
Alexey Martemyanov
2026-05-17 13:03:57 +06:00
parent c9e2980e7a
commit e6742333d1
@@ -0,0 +1,189 @@
---
title: Media Pipeline — File Routing & Directory Policies
created: '2026-05-17'
updated: '2026-05-17'
status: planning
tags:
- media-pipeline
- file-routing
- transmission
- config
---
# Media Pipeline — File Routing & Directory Policies
## Текущее состояние (до реализации)
Текущий конфиг уже содержит:
- `directories` — список папок для сканирования (downloads, movies, cartoons, series, documentaries)
- `transmission.sorting_rules` — genre-based routing для downloads → destination
- `transmission.default_movies_destination` / `default_series_destination`
- `output` — куда идут symlink'и library (movies, series, cartoons, documentaries)
**Проблема:** политика действий с файлами захардкодена в `MediaFinalizer`:
- для всех директорий → всегда `symlink` в library
- нет способа сказать "для music — не делать canonical symlink"
- нет способа сказать "для downloads — переместить через Transmission; для movies — оставить на месте"
- `FileAction.move` и `TransmissionRPC.moveTorrent` существуют, но нигде не вызываются из apply-фазы
## Цель
Политика обработки файлов задаётся **в конфиге per-directory**, не в коде.
`MediaFinalizer` читает политику и генерирует правильный набор `FileAction`.
## Дизайн конфига
```json
"directory_policies": [
{
"path": "/media/downloads",
"unsorted": true,
"on_match": {
"move": "transmission",
"destination_by_kind": {
"movie": "/media/movies",
"series": "/media/series",
"cartoon": "/media/cartoons",
"documentary": "/media/documentaries",
"music": "/media/music",
"audiobook": "/media/audiobooks",
"book": "/media/books",
"game": "/media/games"
},
"library_symlink": true,
"nfo": true
},
"on_unmatched": {
"move": "none"
}
},
{
"path": "/media/movies",
"on_match": {
"move": "none",
"library_symlink": true,
"nfo": true
}
},
{
"path": "/media/cartoons",
"on_match": {
"move": "none",
"library_symlink": true,
"nfo": true
}
},
{
"path": "/media/series",
"on_match": {
"move": "none",
"library_symlink": true,
"nfo": true
}
},
{
"path": "/media/documentaries",
"on_match": {
"move": "none",
"library_symlink": true,
"nfo": true
}
},
{
"path": "/media/music",
"on_match": {
"move": "none",
"library_symlink": false,
"nfo": true
}
}
]
```
### `move` values
- `"transmission"` — вызвать `TransmissionRPC.moveTorrent`; если торрент не найден → fallback на `"filesystem"`
- `"filesystem"``FileAction.move` (прямое перемещение)
- `"none"` — файл остаётся на месте
### `library_symlink`
- `true` — создать каноническое имя в `output.*` папке (как сейчас)
- `false` — не создавать symlink (файл сам по себе — конечная точка, напр. music)
### `nfo`
- `true` — писать NFO рядом с файлом (если нет library_symlink) или в library (если есть)
- `false` — не писать NFO (игры, книги — нет Kodi-совместимой схемы)
### `destination_by_kind`
Только для `unsorted: true` директорий (downloads). Для остальных не нужно — файл уже в правильном месте.
`MediaKind` определяется из resolved metadata (TMDb genre → kind mapping) или из категории торрента.
### `on_unmatched`
Что делать если resolve не удался. По умолчанию `move: "none"` — файл остаётся в downloads.
## Совместимость с текущим конфигом
Старые поля (`transmission.sorting_rules`, `transmission.default_movies_destination`, `output`) сохраняются как fallback. `directory_policies` приоритетнее если задан.
## Компоненты реализации
### 1. `DirectoryPolicy` struct (Config.swift)
```swift
public struct DirectoryPolicy: Codable {
public let path: String
public let unsorted: Bool // defaults false
public let onMatch: MatchPolicy
public let onUnmatched: UnmatchedPolicy
}
public struct MatchPolicy: Codable {
public let move: MoveStrategy // .transmission / .filesystem / .none
public let destinationByKind: [String: String]? // only for unsorted
public let librarySymlink: Bool // defaults true
public let nfo: Bool // defaults true
}
public enum MoveStrategy: String, Codable {
case transmission, filesystem, none = "none"
}
```
### 2. `FileAction.transmissionMove` (FileAction.swift)
```swift
case transmissionMove(torrentId: Int, from: URL, to: URL)
```
### 3. `ApplyExecutor` — выполнение `transmissionMove`
Нужен `TransmissionRPC` в контексте apply-фазы.
При ошибке RPC — fallback на `FileAction.move`.
### 4. `MediaFinalizer` — читает политику из конфига
```swift
func policy(for sourceURL: URL, config: Config) -> DirectoryPolicy?
```
Вместо hardcoded `symlink` генерирует actions по политике.
### 5. `MediaKind` detection
- Из TMDb genres через mapping → `movie`, `series`, `cartoon`, `documentary`
- Из Rutracker категории торрента → `music`, `game`, `audiobook`, `book`
- Fallback: по исходной директории (`/media/music/``music`)
## Порядок реализации
- [ ] **C1**`DirectoryPolicy` в `Config.swift` + парсинг из JSON
- [ ] **C2**`FileAction.transmissionMove` + выполнение в `ApplyExecutor`
- [ ] **C3**`MediaFinalizer` — routing по политике (move + symlink + nfo)
- [ ] **C4**`MediaKind` detection из resolved metadata
- [ ] **C5** — Обновить конфиг на Кракене
- [ ] **C6** — Тесты: downloads → movies (Transmission), movies (no move, symlink), music (no move, no symlink)
## Связанные файлы
- `Sources/MediaPipeline/Config/Config.swift` — добавить `DirectoryPolicy`
- `Sources/MediaPipeline/Apply/FileAction.swift``transmissionMove` case
- `Sources/MediaPipeline/Apply/ApplyExecutor.swift` (или аналог) — выполнение RPC
- `Sources/MediaPipeline/Apply/MediaFinalizer.swift` — routing по политике
- `Sources/MediaSources/APIs/TransmissionRPC.swift` — уже есть `moveTorrent`
## Связанные планы
- [[media-pipeline-tracker-parsers]] — multi-tracker (реализован)
- Шаг B (новые типы медиа) — блокирует `destination_by_kind` для music/games/books