Обзор системы¶
neegde — это десктопное приложение на Tauri 2. Граница между процессами проходит между бэкендом на Rust (нативный процесс) и фронтендом в WebView (Vue 3 TypeScript). Взаимодействие происходит через Tauri IPC: фронтенд вызывает invoke("command_name", args) и получает сериализованный ответ; бэкенд отправляет события через app.emit("event_name", payload).
Устройство процессов¶
┌─────────────────────────────────────────────────────────────┐
│ WebView (Chromium / WKWebView) │
│ │
│ Vue 3 TypeScript │
│ ├── SearchEngine (engine.ts) реестр provider-ов │
│ ├── SearchSession (session.ts) состояние запроса │
│ ├── Providers (rutracker.ts, soulseek.ts) │
│ ├── Pipeline (normalize/dedup/score/filter) │
│ ├── Stores (queue, entities, search, likes) │
│ └── Components (Player, Results, TorrentView, ...) │
│ │
│ Tauri IPC: invoke() / listen() │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Rust backend (tokio async runtime) │
│ │
│ ├── resolver/ resolver намерения запроса (Brave) │
│ ├── rutracker/ HTTP session, поиск, HTML-парсер │
│ ├── soulseek/ бинарный протокол, передача файлов │
│ ├── vozduxan_stream безопасная обёртка, token bucket-ы │
│ ├── vozduxan_ffi сырые unsafe C-биндинги │
│ ├── torrent_stream/ librqbit export, AppDebugLog │
│ ├── torrent_image обложки через librqbit │
│ └── app_paths канонические пути к данным │
│ │
│ FFI boundary │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ C++ vozduxan (libvozduxan.a, libtorrent) │
│ │
│ ├── Session (libtorrent session) │
│ ├── HTTP server (localhost, Range requests) │
│ ├── Piece priority scheduler (3-tier deadlines) │
│ └── Seek cancellation (seek_generation counter) │
└─────────────────────────────────────────────────────────────┘
Управляемое состояние Tauri¶
Всё общее состояние Rust хранится как управляемое состояние Tauri, зарегистрированное в lib.rs:
app.manage(VozduxanStreamState::new(&app, debug_log.clone()));
app.manage(ResolverState::new(debug_log.clone()));
app.manage(TorrentStreamState::new(&app, debug_log.clone()));
app.manage(TorrentImageState::new(&app, debug_log.clone()));
app.manage(RutrackerState::new(&app));
app.manage(SoulseekState::new());
Обработчики Tauri-команд получают состояние через параметры tauri::State<'_, T> — фреймворк извлекает их из карты управляемых состояний.
Критический инвариант: VozduxanStreamState, TorrentImageState и TorrentStreamState должны использовать один и тот же Arc<AppDebugLog>. Они связываются вместе в lib.rs:
let debug_log = Arc::new(AppDebugLog::new(...));
let vzd = VozduxanStreamState::new(&app, debug_log.clone());
let img = TorrentImageState::new(&app, debug_log.clone());
let ts = TorrentStreamState::new(&app, debug_log.clone());
Отдельный Arc для любого из них означал бы, что его логи попадают в другой ring buffer и не отображаются в консоли отладки приложения.
Путь воспроизведения аудио¶
Пользователь кликает трек
│
▼ invoke("torrent_prepare_stream", { magnet, fileIdx, torrentFileB64 })
│
├── [путь RuTracker]
│ vozduxan C++ ──→ libtorrent: открыть торрент, планирование приоритетов
│ ──→ localhost HTTP server: предбуферизация 32–512 KB
│ ──→ return { url: "http://127.0.0.1:PORT/stream/TOKEN" }
│
└── [путь SoulSeek]
invoke("soulseek_prepare_stream", { username, filepath, filesize })
──→ TCP к peer: скачивание во временный файл
──→ localhost HTTP server
──→ return { url: "http://127.0.0.1:PORT/..." }
│
▼
<audio src={url} /> начинает воспроизведение
│
├── событие timeupdate
│ invoke("vozduxan_notify_position", { token, byteOffset })
│ ──→ C++: сдвигает окно приоритетов кусков
│
└── смена трека / размонтирование компонента
invoke("torrent_release_stream", { token })
──→ C++: ожидает завершения потока приоритетов (~100 мс), помечает stream как idle
Путь поиска¶
Пользователь вводит запрос
│
▼ store.runSearch(query)
│
▼ engine.query(rawQuery, enabled)
│
├── resolveQuery(rawQuery) [Tauri IPC → Rust]
│ Brave Search → парсинг заголовков страниц Genius
│ return { canonical: { artist, title }, intent, candidates }
│
├── формирование providerQ = "Artist Title" (скобки убраны)
│
└── new SearchSession(providerQ, { providers, pipeline })
│
├── [RuTracker provider, параллельно]
│ rutracker_search(query) → строки topic-ов
│ для каждого topic: getTorrentDetails → entities Album + Track
│ отправка snapshot-ов по мере завершения topic-ов
│
└── [SoulSeek provider, параллельно]
soulseekSearch(query, requestId) → запуск поиска на бэкенде
listen("soulseek-search-batch", ...) → ответы от peer-ов приходят
отправка groupSlskRowsToEntities(raw) при каждом batch
│
▼ на каждом rAF
объединение всех snapshot-ов → pipeline (dedup → score → filter)
→ session.results.value обновляется → Vue перерисовывает
Ответственность модулей¶
| Модуль | Язык | Ответственность |
|---|---|---|
vozduxan (C++) |
C++ | libtorrent session, HTTP server, приоритеты кусков |
vozduxan_ffi.rs |
Rust | Сырые unsafe C-биндинги к vozduxan |
vozduxan_stream.rs |
Rust | Безопасная обёртка, token bucket-ы, Tauri-команды |
resolver/ |
Rust | Канонизация запроса через Brave Search |
rutracker/ |
Rust | HTTP-аутентификация RuTracker, поиск, парсинг HTML |
soulseek/ |
Rust | Бинарный протокол SoulSeek, поиск, передача файлов |
torrent_stream/ |
Rust | Экспорт файлов через librqbit, AppDebugLog |
torrent_image.rs |
Rust | Обложки через временные librqbit-сессии |
app_paths.rs |
Rust | Вспомогательные функции путей (единственный источник правды) |
search/engine.ts |
TypeScript | Реестр provider-ов, cache сессий |
search/session.ts |
TypeScript | Состояние запроса, rAF-сброс |
search/providers/ |
TypeScript | Реализации provider-ов RuTracker и SoulSeek |
search/pipeline/ |
TypeScript | Стадии normalize / dedup / score / filter |
stores/ |
TypeScript | Реактивное состояние Vue (queue, entities, search, likes) |
persistence/ |
TypeScript | Сериализация localStorage |