diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index eebe8e1..7b15f04 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -263,12 +263,47 @@ pub struct ApiAddTorrentResponse { pub details: TorrentDetailsResponse, } +fn deserialize_only_files<'de, D>( + deserializer: D, +) -> core::result::Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::de::Error; + + let s = Option::::deserialize(deserializer)?; + let s = match s { + Some(s) => s, + None => return Ok(None), + }; + let list = s + .split(',') + .try_fold(Vec::::new(), |mut acc, c| match c.parse() { + Ok(i) => { + acc.push(i); + Ok(acc) + } + Err(_) => Err(D::Error::custom(format!( + "only_files: failed to parse {:?} as integer", + c + ))), + })?; + if list.is_empty() { + return Err(D::Error::custom( + "only_files: should contain at least one file id", + )); + } + Ok(Some(list)) +} + #[derive(Serialize, Deserialize)] pub struct TorrentAddQueryParams { pub overwrite: Option, pub output_folder: Option, pub sub_folder: Option, pub only_files_regex: Option, + #[serde(deserialize_with = "deserialize_only_files")] + pub only_files: Option>, pub list_only: Option, } @@ -277,6 +312,7 @@ impl TorrentAddQueryParams { AddTorrentOptions { overwrite: self.overwrite.unwrap_or(false), only_files_regex: self.only_files_regex, + only_files: self.only_files, output_folder: self.output_folder, sub_folder: self.sub_folder, list_only: self.list_only.unwrap_or(false), diff --git a/crates/librqbit/src/http_api_client.rs b/crates/librqbit/src/http_api_client.rs index 261a484..033354d 100644 --- a/crates/librqbit/src/http_api_client.rs +++ b/crates/librqbit/src/http_api_client.rs @@ -84,6 +84,7 @@ impl HttpApiClient { let params = TorrentAddQueryParams { overwrite: Some(opts.overwrite), only_files_regex: opts.only_files_regex, + only_files: None, output_folder: opts.output_folder, sub_folder: opts.sub_folder, list_only: Some(opts.list_only), diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 394e8bc..5a6c88e 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, fs::File, io::Read, net::SocketAddr, path::PathBuf, time::Duration}; -use anyhow::Context; +use anyhow::{bail, Context}; use buffers::ByteString; use dht::{Dht, Id20, PersistentDht, PersistentDhtConfig}; use librqbit_core::{ @@ -121,6 +121,7 @@ fn compute_only_files>( #[derive(Default, Clone)] pub struct AddTorrentOptions { pub only_files_regex: Option, + pub only_files: Option>, pub overwrite: bool, pub list_only: bool, pub output_folder: Option, @@ -227,7 +228,7 @@ impl Session { let Magnet { info_hash, trackers, - } = Magnet::parse(&*magnet).context("provided path is not a valid magnet URL")?; + } = Magnet::parse(&magnet).context("provided path is not a valid magnet URL")?; let dht_rx = self .dht @@ -267,9 +268,9 @@ impl Session { AddTorrent::Url(url) if url.starts_with("http://") || url.starts_with("https://") => { - torrent_from_url(&*url).await? + torrent_from_url(&url).await? } - AddTorrent::Url(filename) => torrent_from_file(&*filename)?, + AddTorrent::Url(filename) => torrent_from_file(&filename)?, AddTorrent::TorrentFileBytes(bytes) => { torrent_from_bytes(&bytes).context("error decoding torrent")? } @@ -333,20 +334,39 @@ impl Session { opts: AddTorrentOptions, ) -> anyhow::Result { debug!("Torrent info: {:#?}", &info); - let only_files = if let Some(filename_re) = opts.only_files_regex { - let only_files = compute_only_files(&info, &filename_re)?; - for (idx, (filename, _)) in info.iter_filenames_and_lengths()?.enumerate() { - if !only_files.contains(&idx) { - continue; + + let get_only_files = + |only_files: Option>, only_files_regex: Option, list_only: bool| { + match (only_files, only_files_regex) { + (Some(_), Some(_)) => { + bail!("only_files and only_files_regex are mutually exclusive"); + } + (Some(only_files), None) => { + let total_files = info.iter_file_lengths()?.count(); + for id in only_files.iter().copied() { + if id >= total_files { + anyhow::bail!("file id {} is out of range", id); + } + } + Ok(Some(only_files)) + } + (None, Some(filename_re)) => { + let only_files = compute_only_files(&info, &filename_re)?; + for (idx, (filename, _)) in info.iter_filenames_and_lengths()?.enumerate() { + if !only_files.contains(&idx) { + continue; + } + if !list_only { + info!("Will download {:?}", filename); + } + } + Ok(Some(only_files)) + } + (None, None) => Ok(None), } - if !opts.list_only { - info!("Will download {:?}", filename); - } - } - Some(only_files) - } else { - None - }; + }; + + let only_files = get_only_files(opts.only_files, opts.only_files_regex, opts.list_only)?; if opts.list_only { return Ok(AddTorrentResponse::ListOnly(ListOnlyResponse {