implement search feature

This commit is contained in:
yggverse 2025-09-08 21:49:09 +03:00
parent fdd6e10e3a
commit 96c3df1b41
3 changed files with 119 additions and 84 deletions

View file

@ -22,6 +22,7 @@ librqbit-core = "5.0.0"
plurify = "0.2.0" plurify = "0.2.0"
url = "2.5.7" url = "2.5.7"
urlencoding = "2.1.3" urlencoding = "2.1.3"
regex = "1.11.2"
# development # development
[patch.crates-io] [patch.crates-io]

View file

@ -1,15 +1,13 @@
mod config; mod config;
mod format; mod format;
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 chrono::Local; use chrono::Local;
use clap::Parser; use clap::Parser;
use config::Config; use config::Config;
use librqbit_core::{ use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes};
Id20,
torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes},
};
use log::*; use log::*;
use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream}; use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream};
use std::{ use std::{
@ -17,7 +15,6 @@ use std::{
io::{Read, Write}, io::{Read, Write},
net::{SocketAddr, TcpListener, TcpStream}, net::{SocketAddr, TcpListener, TcpStream},
path::PathBuf, path::PathBuf,
str::FromStr,
sync::Arc, sync::Arc,
thread, thread,
}; };
@ -155,23 +152,16 @@ fn response(
peer: &SocketAddr, peer: &SocketAddr,
stream: &mut TlsStream<TcpStream>, stream: &mut TlsStream<TcpStream>,
) { ) {
use route::Route;
debug!("Incoming request from `{peer}` to `{}`", request.url.path()); debug!("Incoming request from `{peer}` to `{}`", request.url.path());
let path = request.url.path().trim_matches('/');
// try index page let route = Route::from_url(&request.url);
if path.is_empty() {
send( // try index page, including optional page value
&match index( match route {
config, Route::List { page } => send(
public, &match list(config, public, request.url.query(), page) {
request.url.query(),
request.url.query_pairs().find_map(|a| {
if a.0 == "page" {
a.1.parse::<usize>().ok()
} else {
None
}
}),
) {
Ok(data) => response::success::Default { Ok(data) => response::success::Default {
data: data.as_bytes(), data: data.as_bytes(),
meta: response::success::default::Meta { meta: response::success::default::Meta {
@ -193,11 +183,8 @@ fn response(
error!("Internal server error on handle peer `{peer}` request: `{e}`") error!("Internal server error on handle peer `{peer}` request: `{e}`")
} }
}, },
) ),
} Route::Search => send(
// try search
else if path == "search" {
send(
&response::Input::Default(response::input::Default { &response::Input::Default(response::input::Default {
message: Some("Keyword, file, hash...".into()), message: Some("Keyword, file, hash...".into()),
}) })
@ -211,58 +198,55 @@ fn response(
) )
} }
}, },
) ),
} Route::Info(id) => match public.torrent(id) {
// try info page Some(torrent) => send(
else if let Ok(id) = Id20::from_str(path) &match info(config, torrent) {
&& let Some(torrent) = public.torrent(id) Ok(data) => response::success::Default {
{ data: data.as_bytes(),
send( meta: response::success::default::Meta {
&match info(config, torrent) { mime: "text/gemini".to_string(),
Ok(data) => response::success::Default { },
data: data.as_bytes(),
meta: response::success::default::Meta {
mime: "text/gemini".to_string(),
},
}
.into_bytes(),
Err(e) => {
error!("Internal server error on handle peer `{peer}` request: `{e}`");
response::failure::temporary::General {
message: Some("Internal server error".to_string()),
} }
.into_bytes() .into_bytes(),
} Err(e) => {
}, error!("Internal server error on handle peer `{peer}` request: `{e}`");
stream, response::failure::temporary::General {
|result| { message: Some("Internal server error".to_string()),
if let Err(e) = result { }
error!("Internal server error on handle peer `{peer}` request: `{e}`") .into_bytes()
} }
}, },
) stream,
} |result| {
// not found if let Err(e) = result {
else { error!("Internal server error on handle peer `{peer}` request: `{e}`")
warn!( }
"Requested resource `{}` not found by peer `{peer}`", },
request.url.as_str() ),
); None => todo!(),
send( },
&response::Failure::Permanent(response::failure::Permanent::NotFound( Route::NotFound => {
response::failure::permanent::NotFound { message: None }, warn!(
)) "Requested resource `{}` not found by peer `{peer}`",
.into_bytes(), request.url.as_str()
stream, );
|result| { send(
if let Err(e) = result { &response::Failure::Permanent(response::failure::Permanent::NotFound(
error!( response::failure::permanent::NotFound { message: None },
"Internal server error on handle peer `{peer}` request `{}`: `{e}`", ))
request.url.as_str() .into_bytes(),
) stream,
} |result| {
}, if let Err(e) = result {
) error!(
"Internal server error on handle peer `{peer}` request `{}`: `{e}`",
request.url.as_str()
)
}
},
)
}
} }
} }
@ -283,7 +267,7 @@ fn send(data: &[u8], stream: &mut TlsStream<TcpStream>, callback: impl FnOnce(Re
// rotes // rotes
fn index( fn list(
config: &Config, config: &Config,
public: &Public, public: &Public,
keyword: Option<&str>, keyword: Option<&str>,
@ -291,6 +275,10 @@ fn index(
) -> Result<String> { ) -> Result<String> {
use plurify::Plurify; use plurify::Plurify;
fn query(keyword: Option<&str>) -> String {
keyword.map(|k| format!("?{}", k)).unwrap_or_default()
}
let (total, torrents) = public.torrents( let (total, torrents) = public.torrents(
keyword, keyword,
Some((Sort::Modified, Order::Desc)), Some((Sort::Modified, Order::Desc)),
@ -347,18 +335,22 @@ fn index(
if let Some(p) = page { if let Some(p) = page {
b.push(format!( b.push(format!(
"=> /{} Back", "=> {}{} Back",
if p > 2 { if p > 2 {
Some(format!("?page={}", p - 1)) format!("/{}", p - 1)
} else { } else {
None "/".into()
} },
.unwrap_or_default() query(keyword)
)) ))
} }
if page.unwrap_or(1) * public.default_limit < total { if page.unwrap_or(1) * public.default_limit < total {
b.push(format!("=> /?page={} Next", page.map_or(2, |p| p + 1))) b.push(format!(
"=> /{}{} Next",
page.map_or(2, |p| p + 1),
query(keyword)
))
} }
b.push("\n=> /search Search".into()); b.push("\n=> /search Search".into());

42
src/route.rs Normal file
View file

@ -0,0 +1,42 @@
use std::str::FromStr;
use librqbit_core::Id20;
use regex::Regex;
use url::Url;
pub enum Route {
Info(Id20),
List { page: Option<usize> },
NotFound,
Search,
}
impl Route {
pub fn from_url(url: &Url) -> Self {
let p = url.path().to_lowercase();
let q = url.query();
if p.is_empty() {
return Self::List { page: None };
}
if let Ok(id) = Id20::from_str(p.trim_matches('/')) {
return Self::Info(id);
}
if p == "/search" && q.is_none() {
return Self::Search;
}
if Regex::new(r"^/(|search)").unwrap().is_match(&p) {
return Self::List {
page: Regex::new(r"/(\d+)$").unwrap().captures(&p).map(|c| {
c.get(1)
.map_or(1, |p| p.as_str().parse::<usize>().unwrap_or(1))
}),
};
}
Self::NotFound
}
}