diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 8c2b749..95341d3 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -868,7 +868,7 @@ impl Session { ) -> BoxFuture<'a, anyhow::Result> { async move { // Magnet links are different in that we first need to discover the metadata. - let opts = opts.unwrap_or_default(); + let mut opts = opts.unwrap_or_default(); let paused = opts.list_only || opts.paused; @@ -885,6 +885,9 @@ impl Session { let info_hash = magnet .as_id20() .context("magnet link didn't contain a BTv1 infohash")?; + if let Some(so) = magnet.get_select_only() { + opts.only_files = Some(so); + } let peer_rx = self.make_peer_rx( info_hash, diff --git a/crates/librqbit/src/session_persistence/mod.rs b/crates/librqbit/src/session_persistence/mod.rs index f1459f8..952e04f 100644 --- a/crates/librqbit/src/session_persistence/mod.rs +++ b/crates/librqbit/src/session_persistence/mod.rs @@ -41,7 +41,7 @@ impl SerializedTorrent { AddTorrent::TorrentFileBytes(self.torrent_bytes) } else { let magnet = - Magnet::from_id20(self.info_hash, self.trackers.into_iter().collect()).to_string(); + Magnet::from_id20(self.info_hash, self.trackers.into_iter().collect(), self.only_files.clone()).to_string(); AddTorrent::from_url(magnet) }; diff --git a/crates/librqbit/src/tests/e2e.rs b/crates/librqbit/src/tests/e2e.rs index 3b8eda0..b83b680 100644 --- a/crates/librqbit/src/tests/e2e.rs +++ b/crates/librqbit/src/tests/e2e.rs @@ -196,7 +196,7 @@ async fn _test_e2e_download(drop_checks: &DropChecks) { .and_then(|v| v.parse().ok()) .unwrap_or(1usize); - let magnet = Magnet::from_id20(torrent_file.info_hash(), Vec::new()).to_string(); + let magnet = Magnet::from_id20(torrent_file.info_hash(), Vec::new(), None).to_string(); // 3. Start a client with the initial peers, and download the file. for _ in 0..client_iters { diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 203f260..03fed11 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -9,6 +9,7 @@ pub struct Magnet { id20: Option, id32: Option, pub trackers: Vec, + select_only: Option> } impl Magnet { @@ -19,12 +20,16 @@ impl Magnet { pub fn as_id32(&self) -> Option { self.id32 } + pub fn get_select_only(&self) -> Option> { + self.select_only.clone() + } - pub fn from_id20(id20: Id20, trackers: Vec) -> Self { + pub fn from_id20(id20: Id20, trackers: Vec, select_only: Option> ) -> Self { Self { id20: Some(id20), id32: None, trackers, + select_only, } } @@ -38,6 +43,7 @@ impl Magnet { let mut id20: Option = None; let mut id32: Option = None; let mut trackers = Vec::::new(); + let mut files = Vec::::new(); for (key, value) in url.query_pairs() { match key.as_ref() { "xt" => { @@ -54,6 +60,26 @@ impl Magnet { } } "tr" => trackers.push(value.into()), + "so" => { + for file_desc in value.split(',') { + if file_desc.is_empty() { + continue; + } + // Handling ranges of file indices + if let Some((start, end)) = file_desc.split_once('-') { + let start_idx: usize = start.parse()?; + let end_idx: usize = end.parse()?; + if start_idx >= end_idx { + anyhow::bail!("range start must be less than range end"); + } + files.extend(start_idx..=end_idx); + } else { + // Handling single file index + let idx: usize = file_desc.parse()?; + files.push(idx); + } + } + } _ => {} } } @@ -62,6 +88,10 @@ impl Magnet { id20, id32, trackers, + select_only: match files.is_empty() { + true => None, + false => Some(files), + }, }), false => { anyhow::bail!("did not find infohash") @@ -97,6 +127,18 @@ impl std::fmt::Display for Magnet { write_ampersand(f)?; write!(f, "tr={tracker}")?; } + if let Some(select_only) = &self.select_only { + if !select_only.is_empty() { + write_ampersand(f)?; + write!(f, "so=")?; + for (index, file) in select_only.iter().enumerate() { + if index > 0 { + write!(f, ",")?; // Add a comma before all but the first index + } + write!(f, "{}", file)?; + } + } + } Ok(()) } } @@ -133,13 +175,19 @@ mod tests { fn test_magnet_to_string() { let id20 = Id20::from_str("a621779b5e3d486e127c3efbca9b6f8d135f52e5").unwrap(); assert_eq!( - &Magnet::from_id20(id20, Default::default()).to_string(), + &Magnet::from_id20(id20, Default::default(), None).to_string(), "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5" ); assert_eq!( - &Magnet::from_id20(id20, vec!["foo".to_string(), "bar".to_string()]).to_string(), + &Magnet::from_id20(id20, vec!["foo".to_string(), "bar".to_string()], None).to_string(), "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&tr=foo&tr=bar" ); + + assert_eq!( + &Magnet::from_id20(id20, Default::default(), Some(vec![1,2,3])).to_string(), + "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&so=1,2,3" + ); + } }