mirror of
https://github.com/YGGverse/btracker-gemini.git
synced 2026-03-31 17:15:30 +00:00
implement btracker-scrape API, use shared State Arc for initiated components
This commit is contained in:
parent
64ad6f01a8
commit
d9edf977c7
3 changed files with 101 additions and 41 deletions
|
|
@ -12,6 +12,7 @@ repository = "https://github.com/YGGverse/btracker-gemini"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.95"
|
||||||
btracker-fs = { version = "0.2.0", features = ["public"] }
|
btracker-fs = { version = "0.2.0", features = ["public"] }
|
||||||
|
btracker-scrape = "0.1.0"
|
||||||
chrono = "^0.4.20"
|
chrono = "^0.4.20"
|
||||||
clap = { version = "4.5.30", features = ["derive"] }
|
clap = { version = "4.5.30", features = ["derive"] }
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
|
|
@ -27,4 +28,5 @@ regex = "1.11.2"
|
||||||
# development
|
# development
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
btracker-fs = { git = "https://github.com/YGGverse/btracker-fs.git" }
|
btracker-fs = { git = "https://github.com/YGGverse/btracker-fs.git" }
|
||||||
|
btracker-scrape = { git = "https://github.com/YGGverse/btracker-scrape.git" }
|
||||||
# btracker-fs = { path = "../btracker-fs" }
|
# btracker-fs = { path = "../btracker-fs" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::{
|
use std::{
|
||||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -47,4 +47,13 @@ pub struct Config {
|
||||||
/// Default index capacity
|
/// Default index capacity
|
||||||
#[arg(short, long, default_value_t = 1000)]
|
#[arg(short, long, default_value_t = 1000)]
|
||||||
pub capacity: usize,
|
pub capacity: usize,
|
||||||
|
|
||||||
|
/// Bind scrape UDP server
|
||||||
|
///
|
||||||
|
/// * requires `tracker` value(s) to enable scrape features
|
||||||
|
#[arg(long, default_values_t = vec![
|
||||||
|
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
|
||||||
|
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0))
|
||||||
|
])]
|
||||||
|
pub udp: Vec<SocketAddr>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
129
src/main.rs
129
src/main.rs
|
|
@ -4,6 +4,7 @@ mod route;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use btracker_fs::public::{Order, Public, Sort, Torrent};
|
use btracker_fs::public::{Order, Public, Sort, Torrent};
|
||||||
|
use btracker_scrape::*;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes};
|
use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes};
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
@ -11,7 +12,7 @@ use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream};
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
net::{SocketAddr, TcpListener, TcpStream},
|
net::{IpAddr, SocketAddr, TcpListener, TcpStream},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
thread,
|
thread,
|
||||||
|
|
@ -35,8 +36,43 @@ fn main() -> Result<()> {
|
||||||
.init()
|
.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Arc::new(Config::parse());
|
let config = Config::parse();
|
||||||
let public = Arc::new(Public::init(&config.storage, config.limit, config.capacity).unwrap());
|
let state = Arc::new(State {
|
||||||
|
public: Public::init(&config.storage, config.limit, config.capacity).unwrap(),
|
||||||
|
scrape: Scrape::init(
|
||||||
|
config
|
||||||
|
.tracker
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| {
|
||||||
|
u.iter()
|
||||||
|
.map(|url| {
|
||||||
|
use std::str::FromStr;
|
||||||
|
if url.scheme() == "tcp" {
|
||||||
|
todo!("TCP scrape is not implemented")
|
||||||
|
}
|
||||||
|
if url.scheme() != "udp" {
|
||||||
|
todo!("Scheme `{}` is not supported", url.scheme())
|
||||||
|
}
|
||||||
|
SocketAddr::new(
|
||||||
|
IpAddr::from_str(
|
||||||
|
url.host_str()
|
||||||
|
.expect("Required valid host value")
|
||||||
|
.trim_start_matches('[')
|
||||||
|
.trim_end_matches(']'),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
url.port().expect("Required valid port value"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.map(|a| (config.udp, a)),
|
||||||
|
),
|
||||||
|
format_date: config.format_date,
|
||||||
|
name: config.name,
|
||||||
|
description: config.description,
|
||||||
|
tracker: config.tracker,
|
||||||
|
});
|
||||||
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls
|
||||||
let acceptor = TlsAcceptor::new(Identity::from_pkcs12(
|
let acceptor = TlsAcceptor::new(Identity::from_pkcs12(
|
||||||
|
|
@ -48,7 +84,7 @@ fn main() -> Result<()> {
|
||||||
&config.password,
|
&config.password,
|
||||||
)?)?;
|
)?)?;
|
||||||
|
|
||||||
let listener = TcpListener::bind(&config.bind)?;
|
let listener = TcpListener::bind(config.bind)?;
|
||||||
|
|
||||||
info!("Server started on `{}`", config.bind);
|
info!("Server started on `{}`", config.bind);
|
||||||
|
|
||||||
|
|
@ -56,11 +92,10 @@ fn main() -> Result<()> {
|
||||||
match stream {
|
match stream {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
thread::spawn({
|
thread::spawn({
|
||||||
let config = config.clone();
|
let state = state.clone();
|
||||||
let public = public.clone();
|
|
||||||
let peer = stream.peer_addr()?;
|
let peer = stream.peer_addr()?;
|
||||||
let connection = acceptor.accept(stream);
|
let connection = acceptor.accept(stream);
|
||||||
move || handle(config, public, peer, connection)
|
move || handle(state, peer, connection)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => error!("{e}"),
|
Err(e) => error!("{e}"),
|
||||||
|
|
@ -70,8 +105,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle(
|
fn handle(
|
||||||
config: Arc<Config>,
|
state: Arc<State>,
|
||||||
public: Arc<Public>,
|
|
||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
connection: Result<TlsStream<TcpStream>, HandshakeError<TcpStream>>,
|
connection: Result<TlsStream<TcpStream>, HandshakeError<TcpStream>>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -110,7 +144,7 @@ fn handle(
|
||||||
if header_buffer.last().is_some_and(|&b| b == b'\n') {
|
if header_buffer.last().is_some_and(|&b| b == b'\n') {
|
||||||
// header bytes contain valid Gemini **request**
|
// header bytes contain valid Gemini **request**
|
||||||
if let Ok(request) = request::Gemini::from_bytes(&header_buffer) {
|
if let Ok(request) = request::Gemini::from_bytes(&header_buffer) {
|
||||||
return response(request, &config, &public, &peer, &mut stream);
|
return response(request, &state, &peer, &mut stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
// header bytes received but yet could not be parsed,
|
// header bytes received but yet could not be parsed,
|
||||||
|
|
@ -148,8 +182,7 @@ fn handle(
|
||||||
|
|
||||||
fn response(
|
fn response(
|
||||||
request: titanite::request::Gemini,
|
request: titanite::request::Gemini,
|
||||||
config: &Config,
|
state: &State,
|
||||||
public: &Public,
|
|
||||||
peer: &SocketAddr,
|
peer: &SocketAddr,
|
||||||
stream: &mut TlsStream<TcpStream>,
|
stream: &mut TlsStream<TcpStream>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -157,7 +190,7 @@ fn response(
|
||||||
use titanite::response::*;
|
use titanite::response::*;
|
||||||
debug!("Incoming request from `{peer}` to `{}`", request.url.path());
|
debug!("Incoming request from `{peer}` to `{}`", request.url.path());
|
||||||
send(
|
send(
|
||||||
&match Route::from_url(&request.url, public) {
|
&match Route::from_url(&request.url, &state.public) {
|
||||||
Route::File(ref path) => success::Default {
|
Route::File(ref path) => success::Default {
|
||||||
data: &std::fs::read(path).unwrap(),
|
data: &std::fs::read(path).unwrap(),
|
||||||
meta: success::default::Meta {
|
meta: success::default::Meta {
|
||||||
|
|
@ -174,6 +207,8 @@ fn response(
|
||||||
"image/webp"
|
"image/webp"
|
||||||
} else if e == "txt" || e == "log" {
|
} else if e == "txt" || e == "log" {
|
||||||
"text/plain"
|
"text/plain"
|
||||||
|
} else if e == "gemini" || e == "gmi" {
|
||||||
|
"text/gemini"
|
||||||
} else {
|
} else {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +219,7 @@ fn response(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
.into_bytes(),
|
.into_bytes(),
|
||||||
Route::List { page, keyword } => match list(config, public, keyword.as_deref(), page) {
|
Route::List { page, keyword } => match list(state, keyword.as_deref(), page) {
|
||||||
Ok(data) => success::Default {
|
Ok(data) => success::Default {
|
||||||
data: data.as_bytes(),
|
data: data.as_bytes(),
|
||||||
meta: success::default::Meta {
|
meta: success::default::Meta {
|
||||||
|
|
@ -204,8 +239,8 @@ fn response(
|
||||||
message: Some("Keyword, file, hash...".into()),
|
message: Some("Keyword, file, hash...".into()),
|
||||||
})
|
})
|
||||||
.into_bytes(),
|
.into_bytes(),
|
||||||
Route::Info(id) => match public.torrent(id) {
|
Route::Info(id) => match state.public.torrent(id) {
|
||||||
Some(torrent) => match info(config, public, torrent) {
|
Some(torrent) => match info(state, torrent) {
|
||||||
Ok(data) => success::Default {
|
Ok(data) => success::Default {
|
||||||
data: data.as_bytes(),
|
data: data.as_bytes(),
|
||||||
meta: success::default::Meta {
|
meta: success::default::Meta {
|
||||||
|
|
@ -267,12 +302,7 @@ fn send(data: &[u8], stream: &mut TlsStream<TcpStream>, callback: impl FnOnce(Re
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(
|
fn list(state: &State, keyword: Option<&str>, page: Option<usize>) -> Result<String> {
|
||||||
config: &Config,
|
|
||||||
public: &Public,
|
|
||||||
keyword: Option<&str>,
|
|
||||||
page: Option<usize>,
|
|
||||||
) -> Result<String> {
|
|
||||||
use plurify::Plurify;
|
use plurify::Plurify;
|
||||||
|
|
||||||
/// format search keyword as the pagination query
|
/// format search keyword as the pagination query
|
||||||
|
|
@ -280,22 +310,22 @@ fn list(
|
||||||
keyword.map(|k| format!("?{}", k)).unwrap_or_default()
|
keyword.map(|k| format!("?{}", k)).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
let (total, torrents) = public.torrents(
|
let (total, torrents) = state.public.torrents(
|
||||||
keyword,
|
keyword,
|
||||||
Some((Sort::Modified, Order::Desc)),
|
Some((Sort::Modified, Order::Desc)),
|
||||||
page.map(|p| if p > 0 { p - 1 } else { p } * public.default_limit),
|
page.map(|p| if p > 0 { p - 1 } else { p } * state.public.default_limit),
|
||||||
Some(public.default_limit),
|
Some(state.public.default_limit),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut b = Vec::new();
|
let mut b = Vec::new();
|
||||||
|
|
||||||
b.push(format!("# {}\n", config.name));
|
b.push(format!("# {}\n", state.name));
|
||||||
|
|
||||||
if let Some(ref description) = config.description {
|
if let Some(ref description) = state.description {
|
||||||
b.push(format!("{description}\n"));
|
b.push(format!("{description}\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref trackers) = config.tracker {
|
if let Some(ref trackers) = state.tracker {
|
||||||
//b.push(format!("## Connect\n"));
|
//b.push(format!("## Connect\n"));
|
||||||
b.push("```".into());
|
b.push("```".into());
|
||||||
for tracker in trackers {
|
for tracker in trackers {
|
||||||
|
|
@ -321,11 +351,16 @@ fn list(
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
));
|
));
|
||||||
b.push(format!(
|
b.push(format!(
|
||||||
"{} • {} • {}\n",
|
"{} • {} • {}{}\n",
|
||||||
torrent.time.format(&config.format_date),
|
torrent.time.format(&state.format_date),
|
||||||
format::total(&i),
|
format::total(&i),
|
||||||
format::files(&i),
|
format::files(&i),
|
||||||
))
|
state
|
||||||
|
.scrape
|
||||||
|
.scrape(i.info_hash.0)
|
||||||
|
.map(|s| format!(" ↑ {} ↓ {} ⏲ {}", s.seeders, s.peers, s.leechers))
|
||||||
|
.unwrap_or_default()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,11 +369,11 @@ fn list(
|
||||||
b.push(format!(
|
b.push(format!(
|
||||||
"Page {} / {} ({total} {} total)\n",
|
"Page {} / {} ({total} {} total)\n",
|
||||||
page.unwrap_or(1),
|
page.unwrap_or(1),
|
||||||
(total as f64 / public.default_limit as f64).ceil(),
|
(total as f64 / state.public.default_limit as f64).ceil(),
|
||||||
total.plurify(&["torrent", "torrents", "torrents"])
|
total.plurify(&["torrent", "torrents", "torrents"])
|
||||||
));
|
));
|
||||||
|
|
||||||
if page.unwrap_or(1) * public.default_limit < total {
|
if page.unwrap_or(1) * state.public.default_limit < total {
|
||||||
b.push(format!(
|
b.push(format!(
|
||||||
"=> /{}{} Next",
|
"=> /{}{} Next",
|
||||||
page.map_or(2, |p| p + 1),
|
page.map_or(2, |p| p + 1),
|
||||||
|
|
@ -363,7 +398,7 @@ fn list(
|
||||||
Ok(b.join("\n"))
|
Ok(b.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
fn info(state: &State, torrent: Torrent) -> Result<String> {
|
||||||
struct File {
|
struct File {
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
length: u64,
|
length: u64,
|
||||||
|
|
@ -387,25 +422,30 @@ fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
||||||
.name
|
.name
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|n| n.to_string())
|
.map(|n| n.to_string())
|
||||||
.unwrap_or(config.name.clone())
|
.unwrap_or(state.name.clone())
|
||||||
));
|
));
|
||||||
|
|
||||||
b.push(format!(
|
b.push(format!(
|
||||||
"{} • {} • {}\n",
|
"{} • {} • {}{}\n",
|
||||||
torrent.time.format(&config.format_date),
|
torrent.time.format(&state.format_date),
|
||||||
format::total(&i),
|
format::total(&i),
|
||||||
format::files(&i),
|
format::files(&i),
|
||||||
|
state
|
||||||
|
.scrape
|
||||||
|
.scrape(i.info_hash.0)
|
||||||
|
.map(|s| format!(" ↑ {} ↓ {} ⏲ {}", s.seeders, s.peers, s.leechers))
|
||||||
|
.unwrap_or_default()
|
||||||
));
|
));
|
||||||
|
|
||||||
b.push(format!(
|
b.push(format!(
|
||||||
"=> {} Magnet\n",
|
"=> {} Magnet\n",
|
||||||
format::magnet(&i, config.tracker.as_ref())
|
format::magnet(&i, state.tracker.as_ref())
|
||||||
));
|
));
|
||||||
|
|
||||||
if let Some(files) = i.info.files.map(|files| {
|
if let Some(files) = i.info.files.map(|files| {
|
||||||
let mut b = Vec::with_capacity(files.len());
|
let mut b = Vec::with_capacity(files.len());
|
||||||
for f in files {
|
for f in files {
|
||||||
let mut p = std::path::PathBuf::new();
|
let mut p = PathBuf::new();
|
||||||
b.push(File {
|
b.push(File {
|
||||||
length: f.length,
|
length: f.length,
|
||||||
path: match f.full_path(&mut p) {
|
path: match f.full_path(&mut p) {
|
||||||
|
|
@ -423,7 +463,7 @@ fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
||||||
b.push("## Files\n".into());
|
b.push("## Files\n".into());
|
||||||
for file in files {
|
for file in files {
|
||||||
let p = file.path();
|
let p = file.path();
|
||||||
b.push(match public.href(&i.info_hash.as_string(), &p) {
|
b.push(match state.public.href(&i.info_hash.as_string(), &p) {
|
||||||
Some(href) => format!(
|
Some(href) => format!(
|
||||||
"=> {} {} ({})",
|
"=> {} {} ({})",
|
||||||
urlencoding::encode(&href),
|
urlencoding::encode(&href),
|
||||||
|
|
@ -437,3 +477,12 @@ fn info(config: &Config, public: &Public, torrent: Torrent) -> Result<String> {
|
||||||
|
|
||||||
Ok(b.join("\n"))
|
Ok(b.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
description: Option<String>,
|
||||||
|
format_date: String,
|
||||||
|
name: String,
|
||||||
|
public: Public,
|
||||||
|
scrape: Scrape,
|
||||||
|
tracker: Option<Vec<url::Url>>,
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue