implement basic search

This commit is contained in:
yggverse 2025-08-11 16:58:57 +03:00
parent a5133b006b
commit 97ac3d9dce
4 changed files with 82 additions and 11 deletions

View file

@ -67,7 +67,38 @@ table > thead > tr > th {
} }
table > tbody > tr:hover > td { table > tbody > tr:hover > td {
background: var(--background) background: var(--background);
}
form {
margin-top: 16px;
}
form input {
background: var(--item);
border-color: var(--item);
border-radius: 3px;
border-style: solid;
border-width: 1px;
color: var(--default);
opacity: 0.9;
}
form input:hover {
opacity: 1;
}
form input[type="text"] {
padding: 8px;
}
form input[type="text"]:focus {
border-color: var(--separator);
}
form input[type="submit"] {
cursor: pointer;
padding: 8px 16px;
} }
body > * { body > * {

View file

@ -19,8 +19,9 @@ use scraper::{Scrape, Scraper};
use std::str::FromStr; use std::str::FromStr;
use torrent::Torrent; use torrent::Torrent;
#[get("/?<page>")] #[get("/?<search>&<page>")]
fn index( fn index(
search: Option<&str>,
page: Option<usize>, page: Option<usize>,
scraper: &State<Scraper>, scraper: &State<Scraper>,
public: &State<Public>, public: &State<Public>,
@ -39,6 +40,7 @@ fn index(
} }
let (total, torrents) = public let (total, torrents) = public
.torrents( .torrents(
search,
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 } * public.default_limit),
Some(public.default_limit), Some(public.default_limit),
@ -52,6 +54,10 @@ fn index(
context! { context! {
title: { title: {
let mut t = String::new(); let mut t = String::new();
if let Some(q) = search && !q.is_empty() {
t.push_str(q);
t.push_str(S)
}
if let Some(p) = page && p > 1 { if let Some(p) = page && p > 1 {
t.push_str(&format!("Page {p}")); t.push_str(&format!("Page {p}"));
t.push_str(S) t.push_str(S)
@ -64,9 +70,9 @@ fn index(
t t
}, },
meta: meta.inner(), meta: meta.inner(),
back: page.map(|p| uri!(index(if p > 2 { Some(p - 1) } else { None }))), back: page.map(|p| uri!(index(search, if p > 2 { Some(p - 1) } else { None }))),
next: if page.unwrap_or(1) * public.default_limit >= total { None } next: if page.unwrap_or(1) * public.default_limit >= total { None }
else { Some(uri!(index(Some(page.map_or(2, |p| p + 1))))) }, else { Some(uri!(index(search, Some(page.map_or(2, |p| p + 1))))) },
rows: torrents rows: torrents
.into_iter() .into_iter()
.filter_map(|t| match Torrent::from_public(&t.bytes, t.time) { .filter_map(|t| match Torrent::from_public(&t.bytes, t.time) {
@ -90,7 +96,8 @@ fn index(
page.unwrap_or(1), page.unwrap_or(1),
(total as f64 / public.default_limit as f64).ceil(), (total as f64 / public.default_limit as f64).ceil(),
total.plurify(&["torrent", "torrents", "torrents"]) total.plurify(&["torrent", "torrents", "torrents"])
) ),
search
}, },
)) ))
} }
@ -164,6 +171,7 @@ fn rss(meta: &State<Meta>, public: &State<Public>) -> Result<RawXml<String>, Sta
); );
for t in public for t in public
.torrents( .torrents(
None,
Some((Sort::Modified, Order::Desc)), Some((Sort::Modified, Order::Desc)),
None, None,
Some(public.default_limit), Some(public.default_limit),

View file

@ -26,8 +26,8 @@ pub struct Torrent {
} }
pub struct Public { pub struct Public {
pub default_limit: usize,
default_capacity: usize, default_capacity: usize,
pub default_limit: usize,
root: PathBuf, root: PathBuf,
} }
@ -43,8 +43,8 @@ impl Public {
return Err("Public root is not directory".into()); return Err("Public root is not directory".into());
} }
Ok(Self { Ok(Self {
default_limit,
default_capacity, default_capacity,
default_limit,
root: root.canonicalize().map_err(|e| e.to_string())?, root: root.canonicalize().map_err(|e| e.to_string())?,
}) })
} }
@ -62,11 +62,12 @@ impl Public {
pub fn torrents( pub fn torrents(
&self, &self,
keyword: Option<&str>,
sort_order: Option<(Sort, Order)>, sort_order: Option<(Sort, Order)>,
start: Option<usize>, start: Option<usize>,
limit: Option<usize>, limit: Option<usize>,
) -> Result<(usize, Vec<Torrent>), Error> { ) -> Result<(usize, Vec<Torrent>), Error> {
let f = self.files(sort_order)?; let f = self.files(keyword, sort_order)?;
let t = f.len(); let t = f.len();
let l = limit.unwrap_or(t); let l = limit.unwrap_or(t);
let mut b = Vec::with_capacity(l); let mut b = Vec::with_capacity(l);
@ -96,13 +97,40 @@ impl Public {
// Helpers // Helpers
fn files(&self, sort_order: Option<(Sort, Order)>) -> Result<Vec<DirEntry>, Error> { fn files(
&self,
keyword: Option<&str>,
sort_order: Option<(Sort, Order)>,
) -> Result<Vec<DirEntry>, Error> {
let mut b = Vec::with_capacity(self.default_capacity); let mut b = Vec::with_capacity(self.default_capacity);
for entry in fs::read_dir(&self.root)? { for entry in fs::read_dir(&self.root)? {
let e = entry?; let e = entry?;
if e.file_type()?.is_file() && e.path().extension().is_some_and(|e| e == EXTENSION) { let p = e.path();
b.push((e.metadata()?.modified()?, e)) if !p.is_file() || p.extension().is_none_or(|e| e != EXTENSION) {
continue;
} }
if keyword.is_some_and(|k| {
!k.is_empty()
&& !librqbit_core::torrent_metainfo::torrent_from_bytes(
&fs::read(e.path()).unwrap(),
)
.is_ok_and(
|m: librqbit_core::torrent_metainfo::TorrentMetaV1Owned| {
m.info_hash.as_string().contains(k)
|| m.info.name.is_some_and(|n| n.to_string().contains(k))
|| m.info.files.is_some_and(|f| {
f.iter().any(|f| {
let mut p = PathBuf::new();
f.full_path(&mut p)
.is_ok_and(|_| p.to_string_lossy().contains(k))
})
})
},
) // @TODO implement fast in-memory search index
}) {
continue;
}
b.push((e.metadata()?.modified()?, e))
} }
if let Some((sort, order)) = sort_order { if let Some((sort, order)) = sort_order {
match sort { match sort {

View file

@ -14,6 +14,10 @@
{% if meta.trackers %} {% if meta.trackers %}
<div>{% for tracker in meta.trackers %}<code>{{ tracker }}</code>{% endfor %}</div> <div>{% for tracker in meta.trackers %}<code>{{ tracker }}</code>{% endfor %}</div>
{% endif %} {% endif %}
<form action="/" method="GET">
<input type="text" name="search" value="{% if search %}{{ search }}{% endif %}" placeholder="Keyword, file, hash..." />
<input type="submit" value="Search" />
</form>
</header> </header>
<main> <main>
{% block content %}{% endblock content %} {% block content %}{% endblock content %}