From 0d7c1c0c279e0bca1e1f68a2ef653b5151b7516a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 18 Mar 2020 00:34:37 -0700 Subject: [PATCH] Print magnet link to stdout with `--link` Magnet links can be printed to standard output with: imdl torrent create --input PATH --link type: added --- Cargo.lock | 24 +++--- src/magnet_link.rs | 27 +++++++ src/metainfo.rs | 52 +++++++++++- src/subcommand/torrent/create.rs | 134 ++++++++++++++++++++++++++++++- src/subcommand/torrent/link.rs | 7 +- 5 files changed, 224 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96c502eb..f219938a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bstr" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502ae1441a0a5adb8fbd38a5955a6416b9493e92b465de5e4a9bde6a539c2c48" +checksum = "2889e6d50f394968c8bf4240dc3f2a7eb4680844d27308f798229ac9d4725f41" dependencies = [ "memchr", ] @@ -195,9 +195,9 @@ checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" [[package]] name = "doc-comment" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807e5847c39ad6a11eac66de492ed1406f76a260eb8656e8740cad9eabc69c27" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "encode_unicode" @@ -265,9 +265,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "globset" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925aa2cac82d8834e2b2a4415b6f6879757fb5c0928fc445ae76461a12eed8f2" +checksum = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120" dependencies = [ "aho-corasick", "bstr", @@ -379,9 +379,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.67" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" +checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" [[package]] name = "log" @@ -589,9 +589,9 @@ checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" [[package]] name = "regex" -version = "1.3.4" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" +checksum = "8900ebc1363efa7ea1c399ccc32daed870b4002651e0bed86e72d501ebbe0048" dependencies = [ "aho-corasick", "memchr", @@ -601,9 +601,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.16" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" [[package]] name = "remove_dir_all" diff --git a/src/magnet_link.rs b/src/magnet_link.rs index 39ef0e1d..599711c6 100644 --- a/src/magnet_link.rs +++ b/src/magnet_link.rs @@ -8,6 +8,18 @@ pub(crate) struct MagnetLink { } impl MagnetLink { + pub(crate) fn from_metainfo(metainfo: &Metainfo) -> Result { + let mut link = Self::with_infohash(metainfo.infohash()?); + + link.set_name(metainfo.info.name.clone()); + + for tracker in metainfo.trackers() { + link.add_tracker(tracker?); + } + + Ok(link) + } + pub(crate) fn with_infohash(infohash: Infohash) -> MagnetLink { MagnetLink { infohash, @@ -57,12 +69,27 @@ impl MagnetLink { } } +impl Display for MagnetLink { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.to_url()) + } +} + #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn display() { + let link = MagnetLink::with_infohash(Infohash::from_bencoded_info_dict("".as_bytes())); + assert_eq!( + link.to_string(), + "magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709" + ); + } + #[test] fn basic() { let link = MagnetLink::with_infohash(Infohash::from_bencoded_info_dict("".as_bytes())); diff --git a/src/metainfo.rs b/src/metainfo.rs index 06e57daa..57cb45a3 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -90,10 +90,18 @@ impl Metainfo { } pub(crate) fn trackers<'a>(&'a self) -> impl Iterator> + 'a { + let mut seen = HashSet::new(); iter::once(&self.announce) .flatten() .chain(self.announce_list.iter().flatten().flatten()) - .map(|text| text.parse().context(error::AnnounceUrlParse)) + .filter_map(move |text| { + if seen.contains(text) { + None + } else { + seen.insert(text.clone()); + Some(text.parse().context(error::AnnounceUrlParse)) + } + }) } pub(crate) fn infohash(&self) -> Result { @@ -414,4 +422,46 @@ mod tests { representation(value, want); } + + #[test] + fn trackers() { + let mut metainfo = Metainfo { + announce: Some("http://foo".into()), + announce_list: None, + nodes: None, + comment: None, + created_by: None, + creation_date: None, + encoding: None, + info: Info { + private: Some(false), + piece_length: Bytes(1024), + source: None, + name: "NAME".into(), + pieces: PieceList::from_pieces(&["fae50"]), + mode: Mode::Single { + length: Bytes(5), + md5sum: None, + }, + }, + }; + + let trackers = metainfo.trackers().collect::>>().unwrap(); + assert_eq!(trackers, &["http://foo".parse().unwrap()]); + + metainfo.announce_list = Some(vec![ + vec!["http://bar".into(), "http://baz".into()], + vec!["http://foo".into()], + ]); + + let trackers = metainfo.trackers().collect::>>().unwrap(); + assert_eq!( + trackers, + &[ + "http://foo".parse().unwrap(), + "http://bar".parse().unwrap(), + "http://baz".parse().unwrap(), + ], + ); + } } diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index c3d1ff92..6df5953c 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -112,6 +112,11 @@ pub(crate) struct Create { parse(from_os_str) )] input: PathBuf, + #[structopt( + long = "link", + help = "Print created torrent `magnet:` URL to standard output" + )] + print_magnet_link: bool, #[structopt( long = "md5", short = "M", @@ -152,6 +157,13 @@ pub(crate) struct Create { parse(from_os_str) )] output: Option, + #[structopt( + long = "peer", + value_name = "PEER", + help = "Add `PEER` to magnet link.", + requires("print-magnet-link") + )] + peers: Vec, #[structopt( long = "piece-length", short = "p", @@ -391,7 +403,15 @@ impl Create { errln!(env, "\u{2728}\u{2728} Done! \u{2728}\u{2728}")?; if self.show { - TorrentSummary::from_metainfo(metainfo)?.write(env)?; + TorrentSummary::from_metainfo(metainfo.clone())?.write(env)?; + } + + if self.print_magnet_link { + let mut link = MagnetLink::from_metainfo(&metainfo)?; + for peer in self.peers { + link.add_peer(peer); + } + outln!(env, "{}", link)?; } if let OutputTarget::File(path) = output { @@ -2288,4 +2308,116 @@ Content Size 9 bytes assert_matches!(env.run(), Ok(())); } + + #[test] + fn no_print_magnet_link() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + ], + tree: { + foo: "", + }, + }; + + assert_matches!(env.run(), Ok(())); + assert_eq!(env.out(), ""); + } + + #[test] + fn print_magnet_link() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--link", + ], + tree: { + foo: "", + }, + }; + + assert_matches!(env.run(), Ok(())); + assert_eq!( + env.out(), + "magnet:?xt=urn:btih:516735f4b80f2b5487eed5f226075bdcde33a54e&dn=foo\n" + ); + } + + #[test] + fn print_magnet_link_with_announce() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--link", + "--announce", + "http://foo.com/announce", + ], + tree: { + foo: "", + }, + }; + + assert_matches!(env.run(), Ok(())); + assert_eq!( + env.out(), + "magnet:\ + ?xt=urn:btih:516735f4b80f2b5487eed5f226075bdcde33a54e\ + &dn=foo\ + &tr=http://foo.com/announce\n" + ); + } + + #[test] + fn peer_requires_link() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--peer", + ], + tree: { + foo: "", + }, + }; + + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + + #[test] + fn link_with_peers() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--peer", + "foo:1337", + "--peer", + "bar:666", + "--link" + ], + tree: { + foo: "", + }, + }; + + assert_matches!(env.run(), Ok(())); + assert_eq!( + env.out(), + "magnet:?xt=urn:btih:516735f4b80f2b5487eed5f226075bdcde33a54e&dn=foo&x.pe=foo:1337&x.pe=bar:\ + 666\n" + ); + } } diff --git a/src/subcommand/torrent/link.rs b/src/subcommand/torrent/link.rs index 8b2eb332..01a7e1eb 100644 --- a/src/subcommand/torrent/link.rs +++ b/src/subcommand/torrent/link.rs @@ -41,13 +41,8 @@ impl Link { link.set_name(&metainfo.info.name); - let mut trackers = HashSet::new(); for result in metainfo.trackers() { - let tracker = result?; - if !trackers.contains(&tracker) { - trackers.insert(tracker.clone()); - link.add_tracker(tracker); - } + link.add_tracker(result?); } for peer in self.peers {