From ea9c632badbf0e2701ce54fa66edeb8d2a440807 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 9 Sep 2025 14:39:31 +0300 Subject: [PATCH] use shared `btracker-scrape` library, update `btracker-fs` API to v0.2 --- Cargo.toml | 3 +- src/feed.rs | 6 +-- src/main.rs | 25 ++++++------ src/scrape.rs | 15 ++++++++ src/scraper.rs | 40 ------------------- src/scraper/udp.rs | 96 ---------------------------------------------- src/torrent.rs | 11 ++++-- 7 files changed, 40 insertions(+), 156 deletions(-) create mode 100644 src/scrape.rs delete mode 100644 src/scraper.rs delete mode 100644 src/scraper/udp.rs diff --git a/Cargo.toml b/Cargo.toml index b3cb6e2..723813f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ repository = "https://github.com/yggverse/btracker" # homepage = "https://yggverse.github.io" [dependencies] -btracker-fs = { version = "0.1", features = ["public"] } +btracker-fs = { version = "0.2", features = ["public"] } +btracker-scrape = { version = "0.1" } chrono = { version = "0.4.41", features = ["serde"] } clap = { version = "4.5", features = ["derive"] } librqbit-core = "5.0" diff --git a/src/feed.rs b/src/feed.rs index b4a233d..670bae2 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -55,9 +55,9 @@ impl Feed { /// Append `item` to the feed `channel` pub fn push(&mut self, torrent: Torrent) { + let info_hash = torrent.info_hash.as_string(); self.buffer.push_str(&format!( - "{}{}{}", - torrent.info_hash, + "{info_hash}{}{}", escape( &torrent .name @@ -65,7 +65,7 @@ impl Feed { .map(|b| b.to_string()) .unwrap_or("?".into()) // @TODO ), - self.canonical.link(&torrent.info_hash) + self.canonical.link(&info_hash) )); self.buffer.push_str(""); diff --git a/src/main.rs b/src/main.rs index d9e634b..eec7e3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,16 +4,16 @@ extern crate rocket; mod config; mod feed; mod meta; -mod scraper; +mod scrape; mod torrent; use btracker_fs::public::{Order, Public, Sort}; +use btracker_scrape::Scrape; use config::Config; use feed::Feed; use meta::Meta; use rocket::{State, http::Status, response::content::RawXml, serde::Serialize}; use rocket_dyn_templates::{Template, context}; -use scraper::{Scrape, Scraper}; use std::str::FromStr; use torrent::Torrent; @@ -21,7 +21,7 @@ use torrent::Torrent; fn index( search: Option<&str>, page: Option, - scraper: &State, + scrape: &State, public: &State, meta: &State, ) -> Result { @@ -32,7 +32,7 @@ fn index( files: Option, indexed: String, magnet: String, - scrape: Option, + scrape: Option, size: usize, torrent: Torrent, } @@ -82,7 +82,7 @@ fn index( files: torrent.files(), indexed: torrent.time.format(&meta.format_time).to_string(), magnet: torrent.magnet(meta.trackers.as_ref()), - scrape: scraper.scrape(&torrent.info_hash), + scrape: scrape::get(scrape, torrent.info_hash.0), size: torrent.size as usize, // required by `filesizeformat` impl torrent }), @@ -104,10 +104,11 @@ fn index( fn info( info_hash: &str, public: &State, - scraper: &State, + scrape: &State, meta: &State, ) -> Result { - match public.torrent(librqbit_core::Id20::from_str(info_hash).map_err(|_| Status::NotFound)?) { + let i = librqbit_core::Id20::from_str(info_hash).map_err(|_| Status::NotFound)?; + match public.torrent(i) { Some(t) => { #[derive(Serialize)] #[serde(crate = "rocket::serde")] @@ -140,7 +141,7 @@ fn info( .map(|f| { let p = f.path(); F { - href: public.href(&torrent.info_hash, &p), + href: public.href(&torrent.info_hash.as_string(), &p), path: p, size: f.length as usize, // required by `filesizeformat` impl } @@ -149,7 +150,7 @@ fn info( }), indexed: torrent.time.format(&meta.format_time).to_string(), magnet: torrent.magnet(meta.trackers.as_ref()), - scrape: scraper.scrape(&torrent.info_hash), + scrape: scrape::get(scrape, i.0), size: torrent.size as usize, // required by `filesizeformat` impl torrent }, @@ -195,7 +196,7 @@ fn rocket() -> _ { if config.canonical_url.is_none() { warn!("Canonical URL option is required for the RSS feed by the specification!") // @TODO } - let scraper = Scraper::init( + let scrape = Scrape::init( config .scrape .map(|u| { @@ -234,8 +235,8 @@ fn rocket() -> _ { rocket::Config::release_default() } }) - .manage(scraper) - .manage(Public::init(config.public.clone(), config.list_limit, config.capacity).unwrap()) + .manage(scrape) + .manage(Public::init(&config.public, config.list_limit, config.capacity).unwrap()) .manage(Meta { canonical: config.canonical_url, description: config.description, diff --git a/src/scrape.rs b/src/scrape.rs new file mode 100644 index 0000000..be2e708 --- /dev/null +++ b/src/scrape.rs @@ -0,0 +1,15 @@ +#[derive(rocket::serde::Serialize, Default)] +#[serde(crate = "rocket::serde")] +pub struct Result { + pub leechers: u32, + pub peers: u32, + pub seeders: u32, +} + +pub fn get(scrape: &super::Scrape, id: [u8; 20]) -> Option { + scrape.get(id).map(|s| Result { + leechers: s.leechers, + peers: s.peers, + seeders: s.seeders, + }) +} diff --git a/src/scraper.rs b/src/scraper.rs deleted file mode 100644 index 1c46383..0000000 --- a/src/scraper.rs +++ /dev/null @@ -1,40 +0,0 @@ -mod udp; - -use rocket::serde::Serialize; -use std::{net::SocketAddr, str::FromStr}; -use udp::Udp; - -#[derive(Serialize, Default)] -#[serde(crate = "rocket::serde")] -pub struct Scrape { - pub leechers: u32, - pub peers: u32, - pub seeders: u32, -} - -pub struct Scraper { - udp: Option, - // tcp: @TODO -} - -impl Scraper { - pub fn init(udp: Option<(Vec, Vec)>) -> Self { - Self { - udp: udp.map(|(local, remote)| Udp::init(local, remote)), - } - } - - pub fn scrape(&self, info_hash: &str) -> Option { - self.udp.as_ref()?; - let mut t = Scrape::default(); - if let Some(ref u) = self.udp { - let r = u - .scrape(librqbit_core::Id20::from_str(info_hash).ok()?) - .ok()?; // @TODO handle - t.leechers += r.leechers; - t.peers += r.peers; - t.seeders += r.seeders; - } - Some(t) - } -} diff --git a/src/scraper/udp.rs b/src/scraper/udp.rs deleted file mode 100644 index 5a8d157..0000000 --- a/src/scraper/udp.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::Scrape; -use librqbit_core::hash_id::Id20; -use rand::Rng; -use std::{ - io::Error, - net::{SocketAddr, UdpSocket}, - time::Duration, -}; - -struct Route { - socket: UdpSocket, - remote: Vec, -} - -pub struct Udp(Vec); - -impl Udp { - pub fn init(local: Vec, remote: Vec) -> Self { - Self( - local - .into_iter() - .map(|l| { - let socket = UdpSocket::bind(l).unwrap(); - socket - .set_read_timeout(Some(Duration::from_secs(3))) - .unwrap(); - Route { - socket, - remote: if l.is_ipv4() { - remote.iter().filter(|r| r.is_ipv4()).cloned().collect() - } else { - remote.iter().filter(|r| r.is_ipv6()).cloned().collect() - }, - } - }) - .collect(), - ) - } - - pub fn scrape(&self, info_hash: Id20) -> Result { - let mut t = Scrape::default(); - for route in &self.0 { - for remote in &route.remote { - route.socket.send_to(&connection_request(), remote)?; - - let mut b = [0u8; 16]; - if route.socket.recv(&mut b)? < 16 { - todo!() - } - route.socket.send_to( - &scrape_request( - u64::from_be_bytes(b[8..16].try_into().unwrap()), - rand::rng().random::(), - &[info_hash], - ), - remote, - )?; - - let mut b = [0u8; 1024]; - let l = route.socket.recv(&mut b)?; - if l < 20 { - todo!() - } - - t.seeders += u32::from_be_bytes(b[8..12].try_into().unwrap()); - t.leechers += u32::from_be_bytes(b[12..16].try_into().unwrap()); - t.peers += u32::from_be_bytes(b[16..20].try_into().unwrap()); - } - } - Ok(t) - } -} - -fn connection_request() -> Vec { - let mut b = Vec::new(); - b.extend_from_slice(&0x41727101980u64.to_be_bytes()); - b.extend_from_slice(&0u32.to_be_bytes()); - b.extend_from_slice(&rand::rng().random::().to_be_bytes()); - b -} - -fn scrape_request(connection_id: u64, transaction_id: u32, info_hashes: &[Id20]) -> Vec { - let mut b = Vec::new(); - b.extend_from_slice(&connection_id.to_be_bytes()); - b.extend_from_slice(&2u32.to_be_bytes()); - b.extend_from_slice(&transaction_id.to_be_bytes()); - // * up to about 74 torrents can be scraped at once - // https://www.bittorrent.org/beps/bep_0015.html - if info_hashes.len() > 74 { - todo!() - } - for hash in info_hashes { - b.extend_from_slice(&hash.0); - } - b -} diff --git a/src/torrent.rs b/src/torrent.rs index eea2e6a..f944a77 100644 --- a/src/torrent.rs +++ b/src/torrent.rs @@ -2,7 +2,10 @@ mod file; use chrono::{DateTime, Utc}; use file::File; -use librqbit_core::torrent_metainfo::{self, TorrentMetaV1Owned}; +use librqbit_core::{ + Id20, + torrent_metainfo::{self, TorrentMetaV1Owned}, +}; use rocket::serde::Serialize; #[derive(Clone, Debug, Serialize)] @@ -13,7 +16,7 @@ pub struct Torrent { pub created_by: Option, pub creation_date: Option>, pub files: Option>, - pub info_hash: String, + pub info_hash: Id20, pub is_private: bool, pub length: Option, pub name: Option, @@ -29,7 +32,7 @@ impl Torrent { let i: TorrentMetaV1Owned = torrent_metainfo::torrent_from_bytes(bytes).map_err(|e| e.to_string())?; Ok(Torrent { - info_hash: i.info_hash.as_string(), + info_hash: i.info_hash, announce: i.announce.map(|a| a.to_string()), comment: i.comment.map(|c| c.to_string()), created_by: i.created_by.map(|c| c.to_string()), @@ -76,7 +79,7 @@ impl Torrent { } pub fn magnet(&self, trackers: Option<&Vec>) -> String { - let mut b = format!("magnet:?xt=urn:btih:{}", self.info_hash); + let mut b = format!("magnet:?xt=urn:btih:{}", self.info_hash.as_string()); if let Some(ref n) = self.name { b.push_str("&dn="); b.push_str(&urlencoding::encode(n))