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"
url = "2.5.7"
urlencoding = "2.1.3"
regex = "1.11.2"
# development
[patch.crates-io]

View file

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

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
}
}