Перейти к содержанию

Обзор системы

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