Peer Ranking & Result Shaping¶
After a search, neegde has a flat list of SlskFileResult rows from many peers. Before the frontend can display them, the Rust layer converts them to SlskSearchResultRow objects and the frontend pipeline scores and groups them.
Sources: src-tauri/src/soulseek/mod.rs, src/search/providers/soulseek.ts
Row shape: 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 hash¶
The row ID uses a 64-bit FNV-1a hash over the concatenation of username + filepath bytes:
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>"
This gives a stable, collision-resistant ID without a separate UUID crate. The same (username, filepath) pair always maps to the same ID, so the pipeline's dedup stage can correctly discard duplicate rows that arrive in different search batches.
Category: bitrate label¶
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(),
}
}
The category string is used by the frontend to group results by quality tier. "SoulSeek" is the fallback when the peer didn't report a bitrate (common for FLAC and lossless files — the attribute encoding is MP3-centric).
Peer availability signals¶
Three fields from the FileSearchResponse tail are stamped onto every row from a peer:
| Field | Type | Meaning |
|---|---|---|
slots_free |
bool | Peer reports at least one free upload slot |
avg_speed |
u32 | Peer's self-declared average upload speed (bytes/s) |
queue_length |
u64 | Number of uploads queued ahead of a new request |
These fields come once per peer (after the file list), not per file. If the response body was truncated (older clients, zlib tail cut), they default to slots_free=true, avg_speed=0, queue_length=0.
Frontend grouping: groupSlskRowsToEntities¶
The SoulSeek provider (src/search/providers/soulseek.ts) receives SlskSearchResultRow[] batches and calls groupSlskRowsToEntities to convert them to pipeline entities.
The grouping strategy:
- Image rows (
slsk_is_image=true) are extracted first and stored separately as potential cover art for adjacent audio files. - Audio rows are grouped by their folder (everything before the last
\or/infilepath). Files in the same folder are assumed to be tracks from the same album. - Each folder group becomes either:
- An album entity (if the folder has ≥ 2 audio files), or
- Individual track entities (if only one file).
- Image files from the same folder are attached as
coverRefto the album entity.
The resulting entities follow the pipeline entity shape with type: "album" or type: "track" and a sources entry with kind: "soulseek".
Peer selection at playback time¶
When the user clicks a SoulSeek track, the frontend calls soulseek_prepare_stream with the username and filepath from the chosen SlskSearchResultRow. There is no automatic peer selection — the user's click determines which peer is contacted.
However, the pipeline's search results are ordered so that better peers appear first. The default pipeline sort (insertion order → provider order) means that within the SoulSeek provider, results arrive sorted by:
- Arrival order — the first peer to respond gets their results shown first.
- Batch order — within a batch from one peer, files are in the peer's filesystem order.
The slots_free, avg_speed, and queue_length fields are available in the entity's raw source data for a future re-ranking pass, but as of the current version the scoring stage is a no-op (all scores are 0).
Cover disk cache¶
SoulSeek cover previews are cached in soulseek/covers/ to avoid P2P traffic on repeated opens:
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"))
}
The cache file contains a JSON-serialized SlskCoverPreview { mime, base64 }. The MD5 of the key is used as the filename to avoid filesystem-unsafe characters in usernames or paths.
On cache hit, soulseek_cover_preview returns immediately without establishing any peer connection. On cache miss, it downloads up to 512 KB from the peer and writes the result to disk.