From dff19370d566a0eea0d637df90821d0feef06d35 Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Mon, 7 Sep 2026 17:57:16 +0200 Subject: [PATCH] feat(lib): report each finished upload's rate to the server The speed other users see for us in `user` lookups and in the server's own ranking comes from SendUploadSpeed (code 121), which the client never sent, so it read zero however much we served. Each completed upload now reports its measured bytes per second, as Nicotine+ and slskd do. --- soulseek-rs-lib/src/client/uploads.rs | 16 ++++++++++++++-- soulseek-rs-lib/src/message/mod.rs | 1 + .../src/message/server/message_factory.rs | 19 +++++++++++++++++++ soulseek-rs-lib/tests/e2e.rs | 16 ++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/soulseek-rs-lib/src/client/uploads.rs b/soulseek-rs-lib/src/client/uploads.rs index 1907691..860a5f9 100644 --- a/soulseek-rs-lib/src/client/uploads.rs +++ b/soulseek-rs-lib/src/client/uploads.rs @@ -1,7 +1,9 @@ use super::{ ActiveUpload, Arc, Client, ClientContext, DownloadStatus, PeerMessage, - RwLock, RwLockExt, collect_failed_tokens, error, next_upload_token, thread, + RwLock, RwLockExt, ServerMessage, collect_failed_tokens, error, + next_upload_token, thread, }; +use crate::message::server::MessageFactory; use crate::types::UploadStatus; use std::sync::atomic::{AtomicBool, AtomicU64}; @@ -156,6 +158,7 @@ impl Client { UploadStatus::Failed(e.to_string()) } }; + let mut report = None; if let Ok(mut ctx) = context.write_safe() && let Some(upload) = ctx.active_uploads.get_mut(&token) { @@ -164,9 +167,18 @@ impl Client { if let Some(bytes) = streamed && secs > 0.0 { - ctx.last_upload_speed = (bytes as f64 / secs) as u32; + let speed = (bytes as f64 / secs) as u32; + ctx.last_upload_speed = speed; + report = ctx.server_sender.clone().map(|s| (s, speed)); } } + // The server folds each finished upload's rate into the average + // speed it shows other users for us. + if let Some((server, speed)) = report { + let _ = server.send(ServerMessage::SendMessage( + MessageFactory::build_send_upload_speed(speed), + )); + } // The slot this transfer held is free now, so whoever is next in // line gets it without waiting for another request to arrive. Self::pump_upload_queue(&context); diff --git a/soulseek-rs-lib/src/message/mod.rs b/soulseek-rs-lib/src/message/mod.rs index 02965e8..28bdf53 100644 --- a/soulseek-rs-lib/src/message/mod.rs +++ b/soulseek-rs-lib/src/message/mod.rs @@ -252,6 +252,7 @@ impl Message { 100 => Ok("AcceptChildren"), 102 => Ok("PossibleParents"), 104 => Ok("WishlistInterval"), + 121 => Ok("SendUploadSpeed"), 160 => Ok("ExcludedSearchPhrases"), 1001 => Ok("CantConnectToPeer"), _ => Err(Error(format!("Unknown server message code: {code}"))), diff --git a/soulseek-rs-lib/src/message/server/message_factory.rs b/soulseek-rs-lib/src/message/server/message_factory.rs index 4676848..87fd62c 100644 --- a/soulseek-rs-lib/src/message/server/message_factory.rs +++ b/soulseek-rs-lib/src/message/server/message_factory.rs @@ -143,6 +143,16 @@ impl MessageFactory { Message::new().write_int32(7).write_string(username).clone() } + /// Tell the server (code 121) what a finished upload averaged, in bytes + /// per second; it folds that into the speed other users see for us. + #[must_use] + pub fn build_send_upload_speed(bytes_per_second: u32) -> Message { + Message::new() + .write_int32(121) + .write_int32(bytes_per_second) + .clone() + } + /// Ask the server (code 36) for a user's share statistics. #[must_use] pub fn build_get_user_stats(username: &str) -> Message { @@ -474,3 +484,12 @@ fn test_build_unwatch_user() { let expect: Vec = [6, 0, 0, 0, 3, 0, 0, 0, b'b', b'o', b'b'].to_vec(); assert_eq!(expect, message.get_data()); } + +#[test] +fn an_upload_speed_report_carries_the_rate() { + let message = MessageFactory::build_send_upload_speed(900); + let mut decoded = Message::new_with_data(message.get_buffer()); + assert_eq!(decoded.get_message_code(), 121); + decoded.set_pointer(8); + assert_eq!(decoded.read_int32(), 900); +} diff --git a/soulseek-rs-lib/tests/e2e.rs b/soulseek-rs-lib/tests/e2e.rs index da235d6..15f4495 100644 --- a/soulseek-rs-lib/tests/e2e.rs +++ b/soulseek-rs-lib/tests/e2e.rs @@ -1769,6 +1769,22 @@ fn two_real_clients_search_and_download() { "a finished upload sets the advertised speed" ); + // The server hears about it too (SendUploadSpeed), which is where every + // other client's `user` lookup and the server's own ranking read it. + let deadline = Instant::now() + Duration::from_secs(10); + let mut recorded = 0; + while Instant::now() < deadline && recorded == 0 { + leecher + .request_user_info("e2e_sharer") + .expect("ask about the sharer"); + std::thread::sleep(Duration::from_millis(250)); + recorded = leecher + .user_info("e2e_sharer") + .and_then(|info| info.stats) + .map_or(0, |stats| stats.average_speed); + } + assert!(recorded > 0, "the server should record the upload's speed"); + let _ = std::fs::remove_dir_all(share_dir); let _ = std::fs::remove_dir_all(download_dir); }