2026-05-17 media-pipeline: file routing plan
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user