2026-05-17 media-pipeline roadmap: актуализация после шагов A-C

This commit is contained in:
Alexey Martemyanov
2026-05-17 18:36:38 +06:00
parent b31da8a0c3
commit a833caffa1
@@ -1,5 +1,5 @@
--- ---
title: Media Pipeline — Multi-Tracker Support title: Media Pipeline — Roadmap
created: '2026-05-17' created: '2026-05-17'
updated: '2026-05-17' updated: '2026-05-17'
status: active status: active
@@ -9,90 +9,77 @@ tags:
- rutor - rutor
- nnm-club - nnm-club
--- ---
# Media Pipeline — Multi-Tracker Support # Media Pipeline — Roadmap
## Текущее состояние ## ✅ Сделано
✅ Реализовано и задеплоено на Kraken: ### Шаг A — Transmission flow + Rutracker парсинг
- `TrackerPageParserRegistry` — dispatch по `url.host`
- `TrackerPageParserRegistry` — dispatch по `url.host`, парсеры: Rutracker, Rutor, NNMClub, PirateBay, Nyaa - `RutrackerPageParser` — CP1251/iconv, SwiftSoup, жанры из заголовка топика, KP/IMDb IDs
- `RutrackerPageParser` — CP1251 decode через `/usr/bin/iconv`, SwiftSoup парсинг - `TrackerMediaKind` — enum: movie/series/cartoon/documentary/music/audiobook/game/book/unknown
- Жанры извлекаются из `[...]` в заголовке топика, страны фильтруются - `extractMediaKind` — детект по `BB.catId` / `FORUM_ID` на странице Rutracker (100+ forum IDs)
- KP/IMDb ID extraction работает - `TorrentPageMetadataFact.mediaKind` + `TorrentLocator.torrentId` — пробрасывается через весь стек
- `TorrentSourceStrategy` использует registry вместо прямого вызова `RutrackerAPI` - `QueryBuilderStrategy``torrentTitle` как предпочитаемый source для поиска
- Fallback NFO при unresolved + `torrentPageMetadata.hasRichData` - Fallback NFO при unresolved + `torrentPageMetadata.hasRichData`
## Архитектура ### Шаг B — Структура парсеров других трекеров
- `RutorPageParser`, `NNMClubPageParser`, `PirateBayPageParser`, `NyaaPageParser` — структура реализована
- Требуют верификации на живых HTML (нет тестовых fixture)
### TrackerPageParser protocol ### Шаг C — DirectoryPolicy + transmissionMove
- `DirectoryPolicy` / `MatchPolicy` / `MoveStrategy` в конфиге — backward-compatible
- `FileAction.transmissionMove(torrentId:, from:, to:)` — Codable
- `ApplyPhase` — async execute, RPC → fallback filesystem
- `buildMovieActions` — читает policy, `needsSymlink`, `needsNFO`, `effectiveOutputDir`
- `kindKey` из реального `TrackerMediaKind``destination_by_kind`
- `transmissionMove` когда есть `torrentId`, иначе `move`
- `config.json` на Кракене — `directory_policies` для всех директорий
- Canonical маркер `.media-pipeline-canonical` для music/books/games пишется в `output` директорию рядом с файлом
```swift ## ⚠️ Частично работает
public protocol TrackerPageParser: Sendable {
var supportedHosts: [String] { get }
func parse(data: Data, url: URL) throws -> TorrentPageMetadata
}
```
### TrackerPageParserRegistry ### Фильмы из downloads
- `.mkv/.mp4` → scanner видит → регион → `transmissionMove``/media/movies/` → canonical marker → повторный run скипает ✅
- `TrackerMediaKind` детектится по forum_id → правильная `destination_by_kind`
```swift ### Музыка/книги/игры из downloads
public struct TrackerPageParserRegistry: Sendable { - Scanner не видит `.mp3/.flac/.epub/.zip` — нет `videoExtensions` для аудио ❌
public static let defaults: [TrackerPageParser] = [ - Регион не создаётся → `buildMovieActions` не вызывается → файл остаётся в downloads
RutrackerPageParser(),
RutorPageParser(),
NNMClubPageParser(),
PirateBayPageParser(),
NyaaPageParser(),
]
public func parser(for url: URL) -> TrackerPageParser?
}
```
## Парсеры по трекерам ## 📋 Следующие шаги
### RutrackerPageParser ✅ ### Шаг D — Audio/non-video scanner (приоритет)
- Encoding: Windows-1251 (через `/usr/bin/iconv` temp files — swift-corelibs-foundation не поддерживает CP1251 на Linux) Чтобы музыка/книги/игры из downloads реально перекладывались:
- Title selector: `#topic-title`, `h1.maintitle`, `a.topictitle`
- **Genres: из заголовка топика `[YEAR, жанр1, жанр2, ..., TECH-TAG]`** — не из поста
- Страны фильтруются через `countryNames: Set<String>` (30+ стран RU+EN)
- Poster: первый img в `div.post_body` не из rutracker UI
- IDs: `imdb.com/title/tt\d+`, `kinopoisk.ru/film/\d+`
### RutorPageParser ✅ (структура реализована) 1. **`AudioClassifier`** — `isAudioFile()` для `.mp3/.flac/.m4a/.ogg/.aac/.wav/.opus`
- Encoding: UTF-8 2. **`NonVideoRegionBuilder`** — создаёт регионы для директорий без video но с audio/doc файлами
- Title: `h1.topic-header` / `div#topic-title` 3. **`FileScanner`** — при наличии `TrackerMediaKind != .movie/.series` (из Transmission метаданных) создавать регион даже без video
- Genres: паттерн `Жанр:` в тексте 4. **`buildAudioActions`** в `MediaFinalizer` — move + NFO рядом, без symlink, canonical marker рядом
5. **Альтернатива (проще)**: unsorted fallback — отдельный pass по `unsorted_dir` через Transmission API, для торрентов без региона вызывать `torrent-set-location` на основе `mediaKind` из страницы трекера
### NNMClubPageParser ✅ (структура реализована) ### Шаг E — Верификация парсеров
- Encoding: Windows-1251 (тот же iconv путь) - HTML fixture тесты для rutor, nnm-club на живых страницах
- Title: `h1.maintitle` / `a.maintitle` - Проверить `extractMediaKind` на реальных forum_id
### PirateBayPageParser ✅ (структура реализована) ### Шаг F — Series из downloads
- Encoding: UTF-8 - `buildSeriesActions` сейчас не читает policy → нет move для сериалов из downloads
- Title: `<h1 id="title">` - Нужно прокинуть `torrentURLFacts`/`torrentPageFacts` аналогично movie
- Нет IMDb/KP ссылок обычно → только title/year из названия торрента
### NyaaPageParser ✅ (структура реализована)
- Encoding: UTF-8
- Аниме: title из `h3.panel-title`, AniDB ID если есть
## Ключевые питфоллы ## Ключевые питфоллы
- `extractGenres` должен парсить из **заголовка топика** (`#topic-title`), а не из тела поста — Rutracker кодирует жанры в `[YEAR, жанр, ...]` в title - CP1251: `/usr/bin/iconv` через temp files — единственный способ на Linux
- CP1251 decode: использовать `/usr/bin/iconv` через temp files — единственный способ на Linux - `extractGenres` — из заголовка топика `[YEAR, жанр, ...]`, не из тела поста
- Кеш хранит сырые байты (binary) — `grep` по кешу не работает для CP1251 файлов - `transmissionMove` генерируется только если scanner создал регион (нужен video файл)
- `TrackerParsers.swift` — боевой файл (используется в продакшне); `RutrackerAPI.swift` — legacy, параллельно существует - Canonical marker для music/books: `output == source dir` → повторный run скипает через `agentResolvedInLibrary`
- `allOutputDirs` в PipelineRunner не включает `directoryPolicies[].output` → нужно обновить
## Файлы ## Файлы
- `Sources/MediaSources/APIs/TrackerParsers.swift` все парсеры + registry (боевой) - `Sources/MediaSources/APIs/TrackerParsers.swift` — парсеры + registry + `extractMediaKind`
- `Sources/MediaSources/APIs/RutrackerAPI.swift` — legacy парсер (используется в части flow) - `Sources/MediaDomain/Facts/TorrentPageMetadataFact.swift``TrackerMediaKind`, поля
- `Sources/MediaSources/Providers/TorrentSourceStrategy.swift` — dispatch через registry - `Sources/MediaDomain/Facts/TorrentInfoURLFact.swift``torrentId: Int?`
- `Sources/MediaDomain/Facts/TorrentPageMetadataFact.swift` — модель: title, year, genres, posterURL, description, externalLinks - `Sources/MediaSources/Protocol/TorrentInfoProvider.swift``TorrentLocator.torrentId`
- `Sources/MediaPipeline/Config/Config.swift``DirectoryPolicy`, `MatchPolicy`, `MoveStrategy`
## Roadmap - `Sources/MediaPipeline/Apply/FileAction.swift``transmissionMove` case
- `Sources/MediaPipeline/Apply/ApplyPhase.swift` — async execute, RPC fallback
- [ ] Шаг B — новые типы медиа: игры, музыка, книги (следующий) - `Sources/MediaPipeline/Apply/MediaFinalizer.swift` — policy-aware routing, torrent extractors
- [ ] Реальные HTML fixture тесты для каждого трекера (rutor, nnm-club)
- [ ] Верификация RutorPageParser/NNMClubPageParser на живых торрентах
- [ ] `MediaKind` detection по категории торрента (напр. "Игры PC" → `.game`)