Ранжирование peer и формирование результатов¶
После поиска у neegde есть плоский список строк SlskFileResult от множества peer. Прежде чем фронтенд сможет их отобразить, Rust-слой преобразует их в объекты SlskSearchResultRow, а pipeline фронтенда оценивает и группирует их.
Источники: src-tauri/src/soulseek/mod.rs, src/search/providers/soulseek.ts
Форма строки: SlskSearchResultRow¶
pub struct SlskSearchResultRow {
// Standard fields (shared with RuTracker SearchResult shape)
pub id: String, // "slsk_<fnv1a_hex(username+filepath)>"
pub name: String, // filename only (last path component)
pub category: String, // "MP3 320 kbps" / "MP3 128 kbps" / "SoulSeek"
pub size: u64,
pub seeders: u64, // always 1 (used as presence indicator)
pub leechers: u64, // always 0
pub added: String, // always "—"
pub source: String, // "soulseek"
// SoulSeek-specific extras
pub slsk_username: String,
pub slsk_filepath: String,
pub bitrate: Option<u32>,
pub duration: Option<u32>,
pub slsk_is_image: bool, // true for .jpg/.png rows (folder cover art)
pub slots_free: bool, // peer has at least one free upload slot
pub avg_speed: u32, // bytes/s declared by peer
pub queue_length: u64, // pending uploads ahead of us (0 = free)
}
ID: хеш FNV-1a¶
ID строки использует 64-битный хеш FNV-1a над конкатенацией байт username + filepath:
fn stable_id(username: &str, filepath: &str) -> String {
let mut h: u64 = 0xcbf29ce484222325;
for b in username.bytes().chain(filepath.bytes()) {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
format!("{h:016x}")
}
// id = "slsk_<16-hex-chars>"
Это даёт стабильный, устойчивый к коллизиям ID без отдельного crate для UUID. Одна и та же пара (username, filepath) всегда отображается на один и тот же ID, поэтому стадия дедупликации pipeline может корректно отбрасывать дублирующие строки, поступающие в разных батчах поиска.
Category: метка bitrate¶
fn bitrate_category(bitrate: Option<u32>) -> String {
match bitrate {
Some(b) if b >= 320 => format!("MP3 {b} kbps"),
Some(b) if b >= 128 => format!("MP3 {b} kbps"),
Some(b) => format!("{b} kbps"),
None => "SoulSeek".to_string(),
}
}
Строка category используется фронтендом для группировки результатов по уровням качества. «SoulSeek» — запасной вариант, когда peer не сообщил bitrate (характерно для FLAC и lossless-файлов — кодирование атрибутов ориентировано на MP3).
Сигналы доступности peer¶
Три поля из хвоста FileSearchResponse записываются в каждую строку от peer:
| Поле | Тип | Значение |
|---|---|---|
slots_free |
bool | Peer сообщает как минимум об одном свободном slot для загрузки |
avg_speed |
u32 | Средняя скорость отдачи peer (байт/с), объявленная им самим |
queue_length |
u64 | Количество загрузок в очереди перед новым запросом |
Эти поля приходят один раз на peer (после списка файлов), а не на каждый файл. Если тело ответа было усечено (старые клиенты, обрезанный хвост zlib), они по умолчанию принимают значения slots_free=true, avg_speed=0, queue_length=0.
Группировка на фронтенде: groupSlskRowsToEntities¶
Провайдер SoulSeek (src/search/providers/soulseek.ts) получает батчи SlskSearchResultRow[] и вызывает groupSlskRowsToEntities для преобразования их в сущности pipeline.
Стратегия группировки:
- Строки с изображениями (
slsk_is_image=true) извлекаются первыми и хранятся отдельно как потенциальные обложки для соседних аудиофайлов. - Аудиострок группируются по папке (всё до последнего
\или/вfilepath). Файлы в одной папке считаются треками одного альбома. - Каждая группа папки становится либо:
- Сущностью альбома (если в папке ≥ 2 аудиофайлов), либо
- Отдельными сущностями треков (если только один файл).
- Изображения из той же папки прикрепляются к сущности альбома как
coverRef.
Полученные сущности следуют форме сущности pipeline с type: "album" или type: "track" и записью sources с kind: "soulseek".
Выбор peer во время воспроизведения¶
Когда пользователь нажимает на трек SoulSeek, фронтенд вызывает soulseek_prepare_stream с username и filepath из выбранной SlskSearchResultRow. Автоматического выбора peer нет — клик пользователя определяет, с каким peer устанавливается связь.
Однако результаты поиска в pipeline упорядочены так, чтобы лучшие peer отображались первыми. Сортировка pipeline по умолчанию (порядок вставки → порядок провайдера) означает, что внутри провайдера SoulSeek результаты отсортированы по:
- Порядку прихода — peer, ответивший первым, показывается выше.
- Порядку батча — внутри батча от одного peer файлы идут в порядке файловой системы peer.
Поля slots_free, avg_speed и queue_length доступны в сырых данных источника сущности для будущего прохода перевзвешивания, но в текущей версии стадия оценки является заглушкой (все оценки равны 0).
Disk cache обложек¶
Превью обложек SoulSeek кэшируются в soulseek/covers/, чтобы избежать P2P-трафика при повторных открытиях:
fn slsk_cover_disk_cache_key(username: &str, filepath: &str) -> String {
let norm = filepath.replace('\\', "/"); // normalize separators
format!("{username}\n{norm}")
}
fn slsk_cover_disk_cache_file(app: &AppHandle, cache_key: &str) -> PathBuf {
let hex_name = format!("{:x}", md5(cache_key.as_bytes()));
slsk_covers_dir(app).join(format!("{hex_name}.json"))
}
Cache-файл содержит сериализованный в JSON SlskCoverPreview { mime, base64 }. MD5 ключа используется как имя файла, чтобы избежать символов, недопустимых в файловой системе, в именах пользователей или путях.
При попадании в cache soulseek_cover_preview возвращается немедленно без установки каких-либо peer-соединений. При промахе загружает до 512 КБ от peer и записывает результат на диск.