Add imdl torrent show
The `imdl torrent show` command displays information about on-disk torrent files. The formatting of the command's output is copied from torf, an excellent command-line torrent creator, editor, and viewer. type: added
This commit is contained in:
parent
6df45e0244
commit
99a069a021
109
Cargo.lock
generated
109
Cargo.lock
generated
|
@ -2,9 +2,9 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.7"
|
version = "0.7.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f56c476256dc249def911d6f7580b5fc7e875895b5d7ee88f5d602208035744"
|
checksum = "743ad5a418686aad3b87fd14c43badd828cf26e214a00f92a384291cf22e1811"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -38,6 +38,12 @@ dependencies = [
|
||||||
"winapi 0.3.8",
|
"winapi 0.3.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
@ -59,6 +65,17 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "2.33.0"
|
version = "2.33.0"
|
||||||
|
@ -75,6 +92,22 @@ dependencies = [
|
||||||
"vec_map",
|
"vec_map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctor"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "difference"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "doc-comment"
|
name = "doc-comment"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -155,9 +188,11 @@ version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term 0.12.1",
|
"ansi_term 0.12.1",
|
||||||
"atty",
|
"atty",
|
||||||
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"libc",
|
"libc",
|
||||||
"md5",
|
"md5",
|
||||||
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_bencode",
|
"serde_bencode",
|
||||||
|
@ -167,6 +202,7 @@ dependencies = [
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
"structopt",
|
"structopt",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"unicode-width",
|
||||||
"url",
|
"url",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
@ -220,6 +256,34 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223"
|
checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "output_vt100"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
|
||||||
|
dependencies = [
|
||||||
|
"winapi 0.3.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -233,10 +297,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
|
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "pretty_assertions"
|
||||||
version = "0.4.6"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a427176b1223957a219780f6bd014fad03f59543ff7feb3854a6fb8e9626ac2b"
|
checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term 0.11.0",
|
||||||
|
"ctor",
|
||||||
|
"difference",
|
||||||
|
"output_vt100",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "875077759af22fa20b610ad4471d8155b321c89c3f2785526c9839b099be4e0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-error-attr",
|
"proc-macro-error-attr",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -247,9 +323,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error-attr"
|
name = "proc-macro-error-attr"
|
||||||
version = "0.4.6"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3e4daa0eae3f30db348ecfa5f942281173d441588ebdac871d730acbfc7f16"
|
checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -471,9 +547,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "structopt"
|
name = "structopt"
|
||||||
version = "0.3.8"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df136b42d76b1fbea72e2ab3057343977b04b4a2e00836c3c7c0673829572713"
|
checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
@ -482,9 +558,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "structopt-derive"
|
name = "structopt-derive"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd50a87d2f7b8958055f3e73a963d78feaccca3836767a9069844e34b5b03c0a"
|
checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
|
@ -568,6 +644,17 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"winapi 0.3.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
|
|
@ -15,9 +15,11 @@ default-run = "imdl"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12"
|
ansi_term = "0.12"
|
||||||
atty = "0.2"
|
atty = "0.2"
|
||||||
|
chrono = "0.4.1"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
md5 = "0.7"
|
md5 = "0.7"
|
||||||
|
pretty_assertions = "0.6"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
serde_bencode = "0.2"
|
serde_bencode = "0.2"
|
||||||
serde_bytes = "0.11"
|
serde_bytes = "0.11"
|
||||||
|
@ -25,6 +27,7 @@ sha1 = "0.6"
|
||||||
snafu = "0.6"
|
snafu = "0.6"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
unicode-width = "0.1"
|
||||||
url = "2"
|
url = "2"
|
||||||
walkdir = "2.1"
|
walkdir = "2.1"
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
- [References](#references)
|
- [References](#references)
|
||||||
- [Alternatives & Prior Art](#alternatives--prior-art)
|
- [Alternatives & Prior Art](#alternatives--prior-art)
|
||||||
- [BitTorrent](#bittorrent)
|
- [BitTorrent](#bittorrent)
|
||||||
|
- [Acknowledgments](#acknowledgments)
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
|
@ -161,3 +162,9 @@ at any time.
|
||||||
| https://wiki.theory.org/index.php/Main_Page | Wiki with lots of information about all aspects of the BitTorrent protocol and implementations. |
|
| https://wiki.theory.org/index.php/Main_Page | Wiki with lots of information about all aspects of the BitTorrent protocol and implementations. |
|
||||||
| https://archive.org/details/2014_torrent_archive_organized) | Massive 158 GiB archive containing 5.5 million torrents, assembled in 2014. |
|
| https://archive.org/details/2014_torrent_archive_organized) | Massive 158 GiB archive containing 5.5 million torrents, assembled in 2014. |
|
||||||
| https://github.com/internetarchive/dweb-transport | Github repository hosting The Internet Archive's distributed web and BitTorrent-related software. |
|
| https://github.com/internetarchive/dweb-transport | Github repository hosting The Internet Archive's distributed web and BitTorrent-related software. |
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
The formatting of `imdl torrent show` is entirely copied from
|
||||||
|
[torf](https://github.com/rndusr/torf-cli), an excellent command-line torrent
|
||||||
|
creator, editor, and viewer.
|
||||||
|
|
2
bin/lint
2
bin/lint
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
! grep --color -REn 'FIXME|TODO|XXX' src
|
! grep --color -REni 'FIXME|TODO|XXX' src
|
||||||
|
|
|
@ -15,15 +15,13 @@ impl<'buffer> Value<'buffer> {
|
||||||
Parser::parse(buffer)
|
Parser::parse(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub(crate) fn encode(&self) -> Vec<u8> {
|
||||||
fn encode(&self) -> Vec<u8> {
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
self.encode_into(&mut buffer);
|
self.encode_into(&mut buffer);
|
||||||
buffer
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub(crate) fn encode_into(&self, buffer: &mut Vec<u8>) {
|
||||||
fn encode_into(&self, buffer: &mut Vec<u8>) {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Int(value) => {
|
Self::Int(value) => {
|
||||||
buffer.push(b'i');
|
buffer.push(b'i');
|
||||||
|
@ -49,7 +47,6 @@ impl<'buffer> Value<'buffer> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn encode_str(buffer: &mut Vec<u8>, contents: &[u8]) {
|
fn encode_str(buffer: &mut Vec<u8>, contents: &[u8]) {
|
||||||
buffer.extend_from_slice(contents.len().to_string().as_bytes());
|
buffer.extend_from_slice(contents.len().to_string().as_bytes());
|
||||||
buffer.push(b':');
|
buffer.push(b':');
|
||||||
|
|
|
@ -13,6 +13,10 @@ const YI: u128 = ZI << 10;
|
||||||
pub(crate) struct Bytes(pub(crate) u128);
|
pub(crate) struct Bytes(pub(crate) u128);
|
||||||
|
|
||||||
impl Bytes {
|
impl Bytes {
|
||||||
|
pub(crate) fn from(bytes: impl Into<u128>) -> Bytes {
|
||||||
|
Bytes(bytes.into())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_power_of_two(self) -> bool {
|
pub(crate) fn is_power_of_two(self) -> bool {
|
||||||
self.0 == 0 || self.0 & (self.0 - 1) == 0
|
self.0 == 0 || self.0 & (self.0 - 1) == 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ pub(crate) use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
// dependencies
|
// dependencies
|
||||||
|
pub(crate) use chrono::{TimeZone, Utc};
|
||||||
pub(crate) use libc::EXIT_FAILURE;
|
pub(crate) use libc::EXIT_FAILURE;
|
||||||
pub(crate) use regex::{Regex, RegexSet};
|
pub(crate) use regex::{Regex, RegexSet};
|
||||||
pub(crate) use serde::{Deserialize, Serialize};
|
pub(crate) use serde::{Deserialize, Serialize};
|
||||||
|
@ -29,6 +30,7 @@ pub(crate) use structopt::{
|
||||||
clap::{AppSettings, ArgSettings},
|
clap::{AppSettings, ArgSettings},
|
||||||
StructOpt,
|
StructOpt,
|
||||||
};
|
};
|
||||||
|
pub(crate) use unicode_width::UnicodeWidthStr;
|
||||||
pub(crate) use url::Url;
|
pub(crate) use url::Url;
|
||||||
pub(crate) use walkdir::WalkDir;
|
pub(crate) use walkdir::WalkDir;
|
||||||
|
|
||||||
|
@ -45,13 +47,10 @@ pub(crate) use crate::{
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
|
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
|
||||||
lint::Lint, metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style,
|
lint::Lint, metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style,
|
||||||
subcommand::Subcommand, torrent::Torrent, use_color::UseColor,
|
subcommand::Subcommand, table::Table, torrent::Torrent, torrent_summary::TorrentSummary,
|
||||||
|
use_color::UseColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// test modules
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) use crate::testing;
|
|
||||||
|
|
||||||
// test stdlib types
|
// test stdlib types
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use std::{
|
pub(crate) use std::{
|
||||||
|
@ -63,6 +62,10 @@ pub(crate) use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// test modules
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use crate::testing;
|
||||||
|
|
||||||
// test structs and enums
|
// test structs and enums
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use crate::{capture::Capture, test_env::TestEnv};
|
pub(crate) use crate::{capture::Capture, test_env::TestEnv};
|
||||||
|
|
17
src/error.rs
17
src/error.rs
|
@ -9,8 +9,13 @@ pub(crate) enum Error {
|
||||||
AnnounceEmpty,
|
AnnounceEmpty,
|
||||||
#[snafu(display("Failed to parse announce URL: {}", source))]
|
#[snafu(display("Failed to parse announce URL: {}", source))]
|
||||||
AnnounceUrlParse { source: url::ParseError },
|
AnnounceUrlParse { source: url::ParseError },
|
||||||
#[snafu(display("Failed to decode bencode: {}", source))]
|
#[snafu(display("Failed to deserialize torrent metainfo from `{}`: {}", path.display(), source))]
|
||||||
BencodeDecode { source: serde_bencode::Error },
|
MetainfoLoad {
|
||||||
|
source: serde_bencode::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
|
||||||
|
MetainfoSerialize { source: serde_bencode::Error },
|
||||||
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
||||||
ByteParse {
|
ByteParse {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -44,8 +49,6 @@ pub(crate) enum Error {
|
||||||
PieceLengthSmall,
|
PieceLengthSmall,
|
||||||
#[snafu(display("Piece length cannot be zero"))]
|
#[snafu(display("Piece length cannot be zero"))]
|
||||||
PieceLengthZero,
|
PieceLengthZero,
|
||||||
#[snafu(display("Serialization failed: {}", source))]
|
|
||||||
Serialize { source: serde_bencode::Error },
|
|
||||||
#[snafu(display("Failed to write to standard error: {}", source))]
|
#[snafu(display("Failed to write to standard error: {}", source))]
|
||||||
Stderr { source: io::Error },
|
Stderr { source: io::Error },
|
||||||
#[snafu(display("Failed to write to standard output: {}", source))]
|
#[snafu(display("Failed to write to standard output: {}", source))]
|
||||||
|
@ -77,12 +80,6 @@ impl From<clap::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serde_bencode::Error> for Error {
|
|
||||||
fn from(source: serde_bencode::Error) -> Self {
|
|
||||||
Self::Serialize { source }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SystemTimeError> for Error {
|
impl From<SystemTimeError> for Error {
|
||||||
fn from(source: SystemTimeError) -> Self {
|
fn from(source: SystemTimeError) -> Self {
|
||||||
Self::SystemTime { source }
|
Self::SystemTime { source }
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::common::*;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Info {
|
pub struct Info {
|
||||||
pub private: u8,
|
pub private: Option<u8>,
|
||||||
#[serde(rename = "piece length")]
|
#[serde(rename = "piece length")]
|
||||||
pub piece_length: u32,
|
pub piece_length: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
@ -7,11 +7,15 @@
|
||||||
clippy::implicit_return,
|
clippy::implicit_return,
|
||||||
clippy::indexing_slicing,
|
clippy::indexing_slicing,
|
||||||
clippy::integer_arithmetic,
|
clippy::integer_arithmetic,
|
||||||
|
clippy::integer_division,
|
||||||
|
clippy::large_enum_variant,
|
||||||
clippy::missing_docs_in_private_items,
|
clippy::missing_docs_in_private_items,
|
||||||
|
clippy::needless_pass_by_value,
|
||||||
clippy::option_map_unwrap_or_else,
|
clippy::option_map_unwrap_or_else,
|
||||||
clippy::option_unwrap_used,
|
clippy::option_unwrap_used,
|
||||||
clippy::result_expect_used,
|
clippy::result_expect_used,
|
||||||
clippy::result_unwrap_used,
|
clippy::result_unwrap_used,
|
||||||
|
clippy::shadow_reuse,
|
||||||
clippy::unreachable,
|
clippy::unreachable,
|
||||||
clippy::wildcard_enum_match_arm
|
clippy::wildcard_enum_match_arm
|
||||||
)]
|
)]
|
||||||
|
@ -62,7 +66,9 @@ mod platform_interface;
|
||||||
mod reckoner;
|
mod reckoner;
|
||||||
mod style;
|
mod style;
|
||||||
mod subcommand;
|
mod subcommand;
|
||||||
|
mod table;
|
||||||
mod torrent;
|
mod torrent;
|
||||||
|
mod torrent_summary;
|
||||||
mod use_color;
|
mod use_color;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -3,13 +3,33 @@ use crate::common::*;
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Metainfo {
|
pub struct Metainfo {
|
||||||
pub announce: String,
|
pub announce: String,
|
||||||
#[serde(rename = "announce list")]
|
#[serde(rename = "announce-list")]
|
||||||
pub announce_list: Option<Vec<Vec<String>>>,
|
pub announce_list: Option<Vec<Vec<String>>>,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
#[serde(rename = "created by")]
|
#[serde(rename = "created by")]
|
||||||
pub created_by: Option<String>,
|
pub created_by: Option<String>,
|
||||||
#[serde(rename = "creation date")]
|
#[serde(rename = "creation date")]
|
||||||
pub creation_date: Option<u64>,
|
pub creation_date: Option<u64>,
|
||||||
pub encoding: String,
|
pub encoding: Option<String>,
|
||||||
pub info: Info,
|
pub info: Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Metainfo {
|
||||||
|
pub(crate) fn _load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
||||||
|
Self::deserialize(path, &bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dump(&self, path: impl AsRef<Path>) -> Result<(), Error> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let bytes = serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize)?;
|
||||||
|
fs::write(path, &bytes).context(error::Filesystem { path })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
serde_bencode::de::from_bytes(&bytes).context(error::MetainfoLoad { path })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,3 +6,12 @@ pub enum Mode {
|
||||||
Single { length: u64, md5sum: Option<String> },
|
Single { length: u64, md5sum: Option<String> },
|
||||||
Multiple { files: Vec<FileInfo> },
|
Multiple { files: Vec<FileInfo> },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
pub(crate) fn total_size(&self) -> Bytes {
|
||||||
|
match self {
|
||||||
|
Self::Single { length, .. } => Bytes::from(*length),
|
||||||
|
Self::Multiple { files } => Bytes::from(files.iter().map(|file| file.length).sum::<u64>()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
172
src/table.rs
Normal file
172
src/table.rs
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct Table {
|
||||||
|
rows: Vec<(&'static str, Value)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self { rows: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn row(&mut self, name: &'static str, value: impl ToString) {
|
||||||
|
self.rows.push((name, Value::Scalar(value.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tiers(
|
||||||
|
&mut self,
|
||||||
|
name: &'static str,
|
||||||
|
tiers: impl IntoIterator<Item = (impl ToString, impl IntoIterator<Item = impl ToString>)>,
|
||||||
|
) {
|
||||||
|
self.rows.push((
|
||||||
|
name,
|
||||||
|
Value::Tiers(
|
||||||
|
tiers
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, values)| {
|
||||||
|
(
|
||||||
|
name.to_string(),
|
||||||
|
values.into_iter().map(|value| value.to_string()).collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rows(&self) -> &[(&'static str, Value)] {
|
||||||
|
&self.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name_width(&self) -> usize {
|
||||||
|
self
|
||||||
|
.rows()
|
||||||
|
.iter()
|
||||||
|
.map(|row| UnicodeWidthStr::width(row.0))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_human_readable(&self, out: &mut dyn Write) -> io::Result<()> {
|
||||||
|
fn padding(out: &mut dyn Write, n: usize) -> io::Result<()> {
|
||||||
|
write!(out, "{:width$}", "", width = n)
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_width = self.name_width();
|
||||||
|
|
||||||
|
for (name, value) in self.rows() {
|
||||||
|
write!(out, "{:>width$}", name, width = name_width)?;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Value::Scalar(value) => writeln!(out, " {}", value)?,
|
||||||
|
Value::Tiers(tiers) => {
|
||||||
|
let tier_name_width = tiers
|
||||||
|
.iter()
|
||||||
|
.map(|(name, _values)| UnicodeWidthStr::width(name.as_str()))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
for (i, (name, values)) in tiers.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
padding(out, name_width)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
" {:width$}",
|
||||||
|
format!("{}:", name).as_str(),
|
||||||
|
width = tier_name_width + 1
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for (i, value) in values.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
padding(out, name_width + 2 + tier_name_width + 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out, " {}", value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Value {
|
||||||
|
Scalar(String),
|
||||||
|
Tiers(Vec<(String, Vec<String>)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn human_readable(table: Table, want: &str) {
|
||||||
|
let mut cursor = Cursor::new(Vec::new());
|
||||||
|
table.write_human_readable(&mut cursor).unwrap();
|
||||||
|
let have = String::from_utf8(cursor.into_inner()).unwrap();
|
||||||
|
if have != want {
|
||||||
|
panic!("have != want:\nHAVE:\n{}\nWANT:\n{}", have, want);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_row() {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.row("Foo", "bar");
|
||||||
|
human_readable(table, "Foo bar\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_rows() {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.row("Foo", "bar");
|
||||||
|
table.row("X", "y");
|
||||||
|
human_readable(table, "Foo bar\n X y\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiers_aligned() {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.tiers("Foo", vec![("Bar", &["a", "b"]), ("Baz", &["x", "y"])]);
|
||||||
|
human_readable(
|
||||||
|
table,
|
||||||
|
"\
|
||||||
|
Foo Bar: a
|
||||||
|
b
|
||||||
|
Baz: x
|
||||||
|
y
|
||||||
|
",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiers_unaligned() {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.tiers(
|
||||||
|
"First",
|
||||||
|
vec![("Some", &["the", "thing"]), ("Other", &["about", "that"])],
|
||||||
|
);
|
||||||
|
table.tiers(
|
||||||
|
"Second",
|
||||||
|
vec![
|
||||||
|
("Row", &["the", "thing"]),
|
||||||
|
("More Stuff", &["about", "that"]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
human_readable(
|
||||||
|
table,
|
||||||
|
" First Some: the
|
||||||
|
thing
|
||||||
|
Other: about
|
||||||
|
that
|
||||||
|
Second Row: the
|
||||||
|
thing
|
||||||
|
More Stuff: about
|
||||||
|
that
|
||||||
|
",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
mod create;
|
mod create;
|
||||||
|
mod show;
|
||||||
mod stats;
|
mod stats;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
|
@ -12,6 +13,7 @@ mod stats;
|
||||||
pub(crate) enum Torrent {
|
pub(crate) enum Torrent {
|
||||||
Create(torrent::create::Create),
|
Create(torrent::create::Create),
|
||||||
Stats(torrent::stats::Stats),
|
Stats(torrent::stats::Stats),
|
||||||
|
Show(torrent::show::Show),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Torrent {
|
impl Torrent {
|
||||||
|
@ -19,6 +21,7 @@ impl Torrent {
|
||||||
match self {
|
match self {
|
||||||
Self::Create(create) => create.run(env),
|
Self::Create(create) => create.run(env),
|
||||||
Self::Stats(stats) => stats.run(env, unstable),
|
Self::Stats(stats) => stats.run(env, unstable),
|
||||||
|
Self::Show(show) => show.run(env),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,7 +197,7 @@ impl Create {
|
||||||
input.parent().unwrap().join(torrent_name)
|
input.parent().unwrap().join(torrent_name)
|
||||||
});
|
});
|
||||||
|
|
||||||
let private = if self.private { 1 } else { 0 };
|
let private = if self.private { Some(1) } else { None };
|
||||||
|
|
||||||
let creation_date = if self.no_creation_date {
|
let creation_date = if self.no_creation_date {
|
||||||
None
|
None
|
||||||
|
@ -227,7 +227,7 @@ impl Create {
|
||||||
|
|
||||||
let metainfo = Metainfo {
|
let metainfo = Metainfo {
|
||||||
comment: self.comment,
|
comment: self.comment,
|
||||||
encoding: consts::ENCODING_UTF8.to_string(),
|
encoding: Some(consts::ENCODING_UTF8.to_string()),
|
||||||
announce: self.announce.to_string(),
|
announce: self.announce.to_string(),
|
||||||
announce_list: if announce_list.is_empty() {
|
announce_list: if announce_list.is_empty() {
|
||||||
None
|
None
|
||||||
|
@ -239,9 +239,7 @@ impl Create {
|
||||||
info,
|
info,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes = serde_bencode::ser::to_bytes(&metainfo)?;
|
metainfo.dump(&output)?;
|
||||||
|
|
||||||
fs::write(&output, bytes).context(error::Filesystem { path: &output })?;
|
|
||||||
|
|
||||||
if self.open {
|
if self.open {
|
||||||
Platform::open(&output)?;
|
Platform::open(&output)?;
|
||||||
|
@ -255,8 +253,6 @@ impl Create {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::test_env::TestEnv;
|
|
||||||
|
|
||||||
fn environment(args: &[&str]) -> TestEnv {
|
fn environment(args: &[&str]) -> TestEnv {
|
||||||
testing::env(["torrent", "create"].iter().chain(args).cloned())
|
testing::env(["torrent", "create"].iter().chain(args).cloned())
|
||||||
}
|
}
|
||||||
|
@ -292,7 +288,7 @@ mod tests {
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||||
assert_eq!(metainfo.info.private, 0);
|
assert_eq!(metainfo.info.private, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -303,7 +299,7 @@ mod tests {
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||||
assert_eq!(metainfo.info.private, 1);
|
assert_eq!(metainfo.info.private, Some(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -573,7 +569,7 @@ mod tests {
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||||
assert_eq!(metainfo.encoding, "UTF-8");
|
assert_eq!(metainfo.encoding, Some("UTF-8".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
90
src/torrent/show.rs
Normal file
90
src/torrent/show.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(StructOpt)]
|
||||||
|
#[structopt(
|
||||||
|
help_message(consts::HELP_MESSAGE),
|
||||||
|
version_message(consts::VERSION_MESSAGE),
|
||||||
|
about("Display information about a `.torrent` file.")
|
||||||
|
)]
|
||||||
|
pub(crate) struct Show {
|
||||||
|
#[structopt(
|
||||||
|
name = "TORRENT",
|
||||||
|
long = "input",
|
||||||
|
help = "Show information about `TORRENT`."
|
||||||
|
)]
|
||||||
|
input: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Show {
|
||||||
|
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
||||||
|
let summary = TorrentSummary::load(&env.resolve(self.input))?;
|
||||||
|
|
||||||
|
let table = summary.table();
|
||||||
|
|
||||||
|
table
|
||||||
|
.write_human_readable(&mut env.out)
|
||||||
|
.context(error::Stdout)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output() {
|
||||||
|
let mut env = testing::env(
|
||||||
|
["torrent", "show", "--input", "foo.torrent"]
|
||||||
|
.iter()
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let metainfo = Metainfo {
|
||||||
|
announce: "announce".into(),
|
||||||
|
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
|
||||||
|
comment: Some("comment".into()),
|
||||||
|
created_by: Some("created by".into()),
|
||||||
|
creation_date: Some(1),
|
||||||
|
encoding: Some("UTF-8".into()),
|
||||||
|
info: Info {
|
||||||
|
private: Some(1),
|
||||||
|
piece_length: 16 * 1024,
|
||||||
|
name: "foo".into(),
|
||||||
|
pieces: vec![
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||||
|
],
|
||||||
|
mode: Mode::Single {
|
||||||
|
length: 20,
|
||||||
|
md5sum: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = env.resolve("foo.torrent");
|
||||||
|
|
||||||
|
metainfo.dump(path).unwrap();
|
||||||
|
|
||||||
|
env.run().unwrap();
|
||||||
|
|
||||||
|
let have = env.out();
|
||||||
|
let want = " Name foo
|
||||||
|
Comment comment
|
||||||
|
Created 1970-01-01 00:00:01 UTC
|
||||||
|
Info Hash bd68a8a5ab377e37e8cdbfd37b670408c59a009f
|
||||||
|
Torrent Size 236 bytes
|
||||||
|
Content Size 20 bytes
|
||||||
|
Private yes
|
||||||
|
Trackers Main: announce
|
||||||
|
Tier 1: announce
|
||||||
|
b
|
||||||
|
Tier 2: c
|
||||||
|
Piece Size 16 KiB
|
||||||
|
Piece Count 1
|
||||||
|
File Count 1
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(have, want);
|
||||||
|
}
|
||||||
|
}
|
104
src/torrent_summary.rs
Normal file
104
src/torrent_summary.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct TorrentSummary {
|
||||||
|
metainfo: Metainfo,
|
||||||
|
infohash: sha1::Digest,
|
||||||
|
size: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TorrentSummary {
|
||||||
|
pub(crate) fn load(path: &Path) -> Result<Self, Error> {
|
||||||
|
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
||||||
|
|
||||||
|
let metainfo = Metainfo::deserialize(path, &bytes)?;
|
||||||
|
|
||||||
|
let value = bencode::Value::decode(&bytes).unwrap();
|
||||||
|
|
||||||
|
let infohash = if let bencode::Value::Dict(items) = value {
|
||||||
|
let info = items
|
||||||
|
.iter()
|
||||||
|
.find(|(key, _value)| key == b"info")
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.encode();
|
||||||
|
Sha1::from(info).digest()
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = path.metadata().context(error::Filesystem { path })?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
size: Bytes(metadata.len().into()),
|
||||||
|
infohash,
|
||||||
|
metainfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn table(&self) -> Table {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table.row("Name", &self.metainfo.info.name);
|
||||||
|
|
||||||
|
if let Some(comment) = &self.metainfo.comment {
|
||||||
|
table.row("Comment", comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(creation_date) = self.metainfo.creation_date {
|
||||||
|
#[allow(clippy::as_conversions)]
|
||||||
|
table.row(
|
||||||
|
"Created",
|
||||||
|
Utc.timestamp(
|
||||||
|
creation_date
|
||||||
|
.min(i64::max_value() as u64)
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.row("Info Hash", self.infohash);
|
||||||
|
|
||||||
|
table.row("Torrent Size", self.size);
|
||||||
|
|
||||||
|
table.row("Content Size", self.metainfo.info.mode.total_size());
|
||||||
|
|
||||||
|
table.row(
|
||||||
|
"Private",
|
||||||
|
if self.metainfo.info.private.unwrap_or(0) == 1 {
|
||||||
|
"yes"
|
||||||
|
} else {
|
||||||
|
"no"
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match &self.metainfo.announce_list {
|
||||||
|
Some(tiers) => {
|
||||||
|
let mut value = Vec::new();
|
||||||
|
value.push(("Main".to_owned(), vec![self.metainfo.announce.clone()]));
|
||||||
|
|
||||||
|
for (i, tier) in tiers.iter().enumerate() {
|
||||||
|
value.push((format!("Tier {}", i + 1), tier.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
table.tiers("Trackers", value);
|
||||||
|
}
|
||||||
|
None => table.row("Tracker", &self.metainfo.announce),
|
||||||
|
}
|
||||||
|
|
||||||
|
table.row("Piece Size", Bytes::from(self.metainfo.info.piece_length));
|
||||||
|
|
||||||
|
table.row("Piece Count", self.metainfo.info.pieces.len() / 20);
|
||||||
|
|
||||||
|
table.row(
|
||||||
|
"File Count",
|
||||||
|
match &self.metainfo.info.mode {
|
||||||
|
Mode::Single { .. } => 1,
|
||||||
|
Mode::Multiple { files } => files.len(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user