--- title: Media Pipeline — File Routing & Directory Policies aliases: - "Медиа пайплайн роутинг" - "Media Pipeline file routing" - "file routing policies" created: '2026-05-17' updated: '2026-05-24' status: planning tags: - media-pipeline - file-routing - transmission - config related: - "[[personal/projects/media-pipeline-tracker-parsers]]" - "[[family/projects/media-toolbox-kraken]]" --- # 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 ## Связанные заметки - [[personal/projects/media-pipeline-tracker-parsers]] - [[family/projects/media-toolbox-kraken]]