diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 8c2b749..14729e3 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,12 @@ 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() { + // Only overwrite opts.only_files if user didn't specify + if opts.only_files.is_none() { + 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..ef62e36 100644 --- a/crates/librqbit/src/session_persistence/mod.rs +++ b/crates/librqbit/src/session_persistence/mod.rs @@ -40,8 +40,12 @@ impl SerializedTorrent { let add_torrent = if !self.torrent_bytes.is_empty() { AddTorrent::TorrentFileBytes(self.torrent_bytes) } else { - let magnet = - Magnet::from_id20(self.info_hash, self.trackers.into_iter().collect()).to_string(); + let magnet = 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..22ca5aa 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,28 @@ impl Magnet { } } "tr" => trackers.push(value.into()), + "so" => { + // Process 'so' values, but silently ignore any which fail parsing + 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 maybe_start_idx: Result = start.parse(); + let maybe_end_idx: Result = end.parse(); + if let (Ok(start_idx), Ok(end_idx)) = (maybe_start_idx, maybe_end_idx) { + files.extend(start_idx..=end_idx); + } + } else { + // Handling single file index + let idx = file_desc.parse(); + if let Ok(idx) = idx { + files.push(idx); + } + } + } + } _ => {} } } @@ -62,6 +90,11 @@ impl Magnet { id20, id32, trackers, + select_only: if files.is_empty() { + None + } else { + Some(files) + }, }), false => { anyhow::bail!("did not find infohash") @@ -97,6 +130,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 +178,18 @@ 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" + ); } }