btpeer 0.9.0

Simple CLI tool and library to get peers from TCP/HTTP and UDP BitTorrent trackers
Documentation
mod config;
mod http;
mod info_hash;
mod peer;
mod udp;

use anyhow::Result;
use clap::Parser;
use colored::Colorize;
use config::*;
use http::query::{Announce, Scrape};
use info_hash::InfoHash;
use std::{
    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket},
    str::FromStr,
    time::Duration,
};
use udp::Server;
use url::Url;

#[tokio::main]
async fn main() -> Result<()> {
    fn print_announce(interval: u32, leechers: u32, seeders: u32, peers: peer::Buffer) {
        println!("{}", format!("Update interval: {interval}s").yellow());
        println!("{}: {seeders}, {}: {leechers}", "S".green(), "L".red());
        if peers.0.is_empty() {
            println!("{}", "No peers.".red());
        } else {
            for peer in peers.0 {
                println!("{peer}")
            }
        }
    }
    fn print_scrape(leechers: u32, peers: u32, seeders: u32) {
        println!(
            "{}: {seeders}, {}: {peers}, {}: {leechers} - total",
            "S".green(),
            "P".yellow(),
            "L".red()
        );
    }
    fn print_full_scrape(leechers: u32, peers: u32, seeders: u32, info_hash: InfoHash) {
        println!(
            "{}: {seeders}, {}: {peers}, {}: {leechers} - {info_hash}",
            "S".green(),
            "P".yellow(),
            "L".red()
        );
    }
    fn parse_udp_socket_address(url: &Url) -> Result<SocketAddr> {
        let host = url
            .host_str()
            .expect("UDP tracker hostname is required to continue");
        Ok(SocketAddr::new(
            match IpAddr::from_str(host.trim_start_matches('[').trim_end_matches(']')) {
                Ok(ip) => ip,
                Err(_) => dns_lookup::lookup_host(host)?
                    .next()
                    .expect("UDP tracker DNS is not reachable"),
            },
            url.port()
                .expect("UDP tracker hostname is required to continue"),
        ))
    }
    fn udp_server_for(address: SocketAddr, port: u16, timeout: Duration) -> Result<UdpSocket> {
        let server = if address.is_ipv6() {
            UdpSocket::bind(SocketAddr::V6(SocketAddrV6::new(
                Ipv6Addr::UNSPECIFIED,
                port,
                0,
                0,
            )))?
        } else {
            UdpSocket::bind(SocketAddr::V4(SocketAddrV4::new(
                Ipv4Addr::UNSPECIFIED,
                port,
            )))?
        };
        server.set_read_timeout(Some(timeout))?;
        server.set_write_timeout(Some(timeout))?;
        Ok(server)
    }
    match Cli::parse().command {
        Command::Announce(config) => {
            for url in &config.tracker {
                match url.scheme() {
                    "http" | "https" => {
                        let query = Announce::new(url.as_str(), &config.info_hash, config.port)?;
                        println!(
                            "{}",
                            format!("Sending HTTP announce to `{query}`...").blue()
                        );
                        if url.host_str().is_some_and(|h| h.ends_with(".i2p")) {
                            match http::announce_i2p(
                                &query,
                                Duration::from_secs(config.timeout),
                                config.proxy_url.as_deref(),
                            )
                            .await
                            {
                                Ok(response) => print_announce(
                                    response.interval,
                                    response.incomplete,
                                    response.complete,
                                    response.peers,
                                ),
                                Err(e) => println!("{}", e.to_string().red()),
                            }
                        } else {
                            match http::announce(
                                &query,
                                Duration::from_secs(config.timeout),
                                config.proxy_url.as_deref(),
                            )
                            .await
                            {
                                Ok(response) => print_announce(
                                    response.interval,
                                    response.incomplete,
                                    response.complete,
                                    response.peers,
                                ),
                                Err(e) => println!("{}", e.to_string().red()),
                            }
                        }
                    }
                    "udp" => {
                        let tracker = parse_udp_socket_address(url)?;
                        println!(
                            "{}",
                            format!("Sending UDP announce to `{tracker}`...").blue()
                        );
                        match udp_server_for(
                            tracker,
                            config.port,
                            Duration::from_secs(config.timeout),
                        )?
                        .announce(&config.info_hash, &tracker)
                        {
                            Ok(response) => print_announce(
                                response.interval,
                                response.leechers,
                                response.seeders,
                                response.peers,
                            ),
                            Err(e) => println!("{}", e.to_string().red()),
                        }
                    }
                    _ => todo!(),
                }
            }
        }
        Command::Scrape(config) => {
            for url in &config.tracker {
                match url.scheme() {
                    "http" | "https" => {
                        let query = Scrape::new(url.as_str(), &config.info_hash)?;
                        println!(
                            "{}",
                            format!("Sending HTTP scrape request to `{query}`...").blue()
                        );
                        match http::scrape(
                            &query,
                            Duration::from_secs(config.timeout),
                            config.proxy_url.as_deref(),
                        )
                        .await
                        {
                            Ok(response) => {
                                print_scrape(
                                    response.total.incomplete,
                                    response.total.complete,
                                    response.total.downloaded,
                                );
                                for (info_hash, total) in response.stats {
                                    print_full_scrape(
                                        total.incomplete,
                                        total.complete,
                                        total.downloaded,
                                        info_hash,
                                    )
                                }
                            }
                            Err(e) => println!("{}", e.to_string().red()),
                        }
                    }
                    "udp" => {
                        for url in &config.tracker {
                            let tracker = parse_udp_socket_address(url)?;
                            println!(
                                "{}",
                                format!("Sending UDP scrape request to `{tracker}`...").blue()
                            );
                            match udp_server_for(
                                tracker,
                                config.port,
                                Duration::from_secs(config.timeout),
                            )?
                            .scrape(&config.info_hash, &tracker)
                            {
                                Ok(response) => {
                                    print_scrape(
                                        response.leechers,
                                        response.peers,
                                        response.seeders,
                                    );
                                }
                                Err(e) => println!("{}", e.to_string().red()),
                            }
                        }
                    }
                    scheme => todo!("{scheme} tracker support yet not implemented"),
                }
            }
        }
    }
    Ok(())
}