WIP: improve search #1
|
@ -16,13 +16,13 @@ use actix_web::{middleware, web, App, HttpResponse, HttpServer};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::{env, cmp, io, fmt};
|
use std::{env, cmp, io};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
const DEFAULT_SIZE: usize = 25;
|
const DEFAULT_SIZE: usize = 25;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Deserialize)]
|
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||||
enum SortKey {
|
enum SortKey {
|
||||||
Name,
|
Name,
|
||||||
Size,
|
Size,
|
||||||
|
@ -37,20 +37,7 @@ impl Default for SortKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SortKey {
|
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let s = match &self {
|
|
||||||
Self::Name => "name",
|
|
||||||
Self::Size => "size_bytes",
|
|
||||||
Self::Seeders => "seeders",
|
|
||||||
Self::Leechers => "leechers",
|
|
||||||
Self::Scraped => "scraped_date",
|
|
||||||
};
|
|
||||||
write!(f, "{}", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Deserialize)]
|
|
||||||
enum SortDirection {
|
enum SortDirection {
|
||||||
Asc,
|
Asc,
|
||||||
Desc,
|
Desc,
|
||||||
|
@ -62,16 +49,6 @@ impl Default for SortDirection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SortDirection {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let s = match &self {
|
|
||||||
Self::Asc => "asc",
|
|
||||||
Self::Desc => "desc",
|
|
||||||
};
|
|
||||||
write!(f, "{}", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
println!("Access me at {}", endpoint());
|
println!("Access me at {}", endpoint());
|
||||||
|
@ -140,6 +117,8 @@ async fn search(
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO what is NewQuery used for?
|
||||||
|
// it is not a file search, that is done through SearchQuery
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct NewQuery {
|
struct NewQuery {
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
|
@ -167,6 +146,29 @@ async fn new_torrents(
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_order_clause(type_: &str, sort_key: SortKey, sort_dir: SortDirection) -> String {
|
||||||
|
let dir = match sort_dir {
|
||||||
|
SortDirection::Asc => "asc",
|
||||||
|
SortDirection::Desc => "desc",
|
||||||
|
};
|
||||||
|
|
||||||
|
let column = match sort_key {
|
||||||
|
SortKey::Name => {
|
||||||
|
if type_ == "file" {
|
||||||
|
"path"
|
||||||
|
} else {
|
||||||
|
"name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SortKey::Size => "size_bytes",
|
||||||
|
SortKey::Seeders => "seeders",
|
||||||
|
SortKey::Leechers => "leechers",
|
||||||
|
SortKey::Scraped => "scraped_date",
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{} {}", column, dir)
|
||||||
|
}
|
||||||
|
|
||||||
fn search_query(
|
fn search_query(
|
||||||
query: web::Query<SearchQuery>,
|
query: web::Query<SearchQuery>,
|
||||||
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
||||||
|
@ -185,7 +187,7 @@ fn search_query(
|
||||||
let offset = size * (page - 1);
|
let offset = size * (page - 1);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"query = {}, type = {}, page = {}, size = {}, sort_key = {}, sort_dir = {}",
|
"query = {}, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}",
|
||||||
q, type_, page, size, sort_key, sort_dir
|
q, type_, page, size, sort_key, sort_dir
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -213,7 +215,7 @@ fn new_query(
|
||||||
let offset = size * (page - 1);
|
let offset = size * (page - 1);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"new, type = {}, page = {}, size = {}, sort_key = {}, sort_dir = {}",
|
"new, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}",
|
||||||
type_, page, size, sort_key, sort_dir
|
type_, page, size, sort_key, sort_dir
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -248,8 +250,9 @@ fn torrent_search(
|
||||||
size: usize,
|
size: usize,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
) -> Result<Vec<Torrent>, Error> {
|
) -> Result<Vec<Torrent>, Error> {
|
||||||
// `sort_key` and `sort_dir` are already sanitized and should not be escaped:
|
let order_clause = build_order_clause("torrent", sort_key, sort_dir);
|
||||||
let stmt_str = format!("select * from torrents where name like '%' || ?1 || '%' order by {} {} limit ?2, ?3", sort_key, sort_dir);
|
// `order_clause` is already sanitized and should not be escaped:
|
||||||
|
let stmt_str = format!("select * from torrents where name like '%' || ?1 || '%' order by {} limit ?2, ?3", order_clause);
|
||||||
let mut stmt = conn.prepare(&stmt_str)?;
|
let mut stmt = conn.prepare(&stmt_str)?;
|
||||||
let torrent_iter = stmt.query_map(
|
let torrent_iter = stmt.query_map(
|
||||||
params![
|
params![
|
||||||
|
@ -334,8 +337,9 @@ fn torrent_file_search(
|
||||||
size: usize,
|
size: usize,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
) -> Result<Vec<File>, Error> {
|
) -> Result<Vec<File>, Error> {
|
||||||
// `sort_key` and `sort_dir` are already sanitized and should not be escaped:
|
let order_clause = build_order_clause("file", sort_key, sort_dir);
|
||||||
let stmt_str = format!("select * from files where path like '%' || ?1 || '%' order by {} {} limit ?2, ?3", sort_key, sort_dir);
|
// `order_clause` is already sanitized and should not be escaped:
|
||||||
|
let stmt_str = format!("select * from files where path like '%' || ?1 || '%' order by {} limit ?2, ?3", order_clause);
|
||||||
let mut stmt = conn.prepare(&stmt_str).unwrap();
|
let mut stmt = conn.prepare(&stmt_str).unwrap();
|
||||||
let file_iter = stmt.query_map(
|
let file_iter = stmt.query_map(
|
||||||
params![
|
params![
|
||||||
|
|
|
@ -8,7 +8,6 @@ interface State {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<any, State> {
|
export class Navbar extends Component<any, State> {
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
searchParams: {
|
searchParams: {
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -80,20 +79,20 @@ export class Navbar extends Component<any, State> {
|
||||||
let searchParams: SearchParams = {
|
let searchParams: SearchParams = {
|
||||||
q: event.target.value,
|
q: event.target.value,
|
||||||
page: 1,
|
page: 1,
|
||||||
sort_key: 'Name',
|
sort_key: i.state.searchParams.sort_key,
|
||||||
sort_dir: 'Desc',
|
sort_dir: i.state.searchParams.sort_dir,
|
||||||
type_: i.state.searchParams.type_
|
type_: i.state.searchParams.type_
|
||||||
}
|
}
|
||||||
i.setState({ searchParams: searchParams });
|
i.setState({ searchParams: searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO why does switching from torrent to file search clear the sort_key?
|
||||||
searchTypeChange(i: Navbar, event) {
|
searchTypeChange(i: Navbar, event) {
|
||||||
console.log("in searchTypeChange");
|
|
||||||
let searchParams: SearchParams = {
|
let searchParams: SearchParams = {
|
||||||
q: i.state.searchParams.q,
|
q: i.state.searchParams.q,
|
||||||
page: 1,
|
page: 1,
|
||||||
sort_key: 'Name',
|
sort_key: i.state.searchParams.sort_key,
|
||||||
sort_dir: 'Desc',
|
sort_dir: i.state.searchParams.sort_dir,
|
||||||
type_: event.target.value
|
type_: event.target.value
|
||||||
}
|
}
|
||||||
i.setState({ searchParams: searchParams });
|
i.setState({ searchParams: searchParams });
|
||||||
|
|
|
@ -11,39 +11,6 @@ interface State {
|
||||||
searching: Boolean;
|
searching: Boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortableLink extends Component {
|
|
||||||
render({ sortKey, state: { searchParams } }) {
|
|
||||||
return <span>
|
|
||||||
<a href={this.buildSearchURL(sortKey, searchParams)}>
|
|
||||||
{sortKey}
|
|
||||||
{this.renderIcon(sortKey, searchParams)}
|
|
||||||
</a>
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSearchURL(key, searchParams) {
|
|
||||||
let page, direction;
|
|
||||||
if (key === searchParams.sort_key) {
|
|
||||||
page = searchParams.page;
|
|
||||||
direction = searchParams.sort_dir === "Asc" ? "Desc" : "Asc";
|
|
||||||
} else {
|
|
||||||
page = 1;
|
|
||||||
direction = "Desc";
|
|
||||||
}
|
|
||||||
return `/#/search/${searchParams.type_}/${searchParams.q}/${page}/${key}/${direction}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon(sortKey, searchParams) {
|
|
||||||
if (searchParams.sort_key !== sortKey) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (searchParams.sort_dir === "Asc") {
|
|
||||||
return <svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg>;
|
|
||||||
}
|
|
||||||
return <svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg>;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export class Search extends Component<any, State> {
|
export class Search extends Component<any, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
results: {
|
results: {
|
||||||
|
@ -64,6 +31,7 @@ export class Search extends Component<any, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
console.log("comp did mount");
|
||||||
this.state.searchParams = {
|
this.state.searchParams = {
|
||||||
page: Number(this.props.match.params.page),
|
page: Number(this.props.match.params.page),
|
||||||
q: this.props.match.params.q,
|
q: this.props.match.params.q,
|
||||||
|
@ -76,6 +44,7 @@ export class Search extends Component<any, State> {
|
||||||
|
|
||||||
// Re-do search if the props have changed
|
// Re-do search if the props have changed
|
||||||
componentDidUpdate(lastProps: any) {
|
componentDidUpdate(lastProps: any) {
|
||||||
|
console.log("comp did update");
|
||||||
if (lastProps.match && lastProps.match.params !== this.props.match.params) {
|
if (lastProps.match && lastProps.match.params !== this.props.match.params) {
|
||||||
this.state.searchParams = {
|
this.state.searchParams = {
|
||||||
page: Number(this.props.match.params.page),
|
page: Number(this.props.match.params.page),
|
||||||
|
@ -144,6 +113,48 @@ export class Search extends Component<any, State> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortLink(sortKey) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<a href={this.buildSortURL(sortKey)}>
|
||||||
|
{sortKey}
|
||||||
|
{this.sortLinkIcon(sortKey)}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSortURL(sortKey) {
|
||||||
|
const searchParams = this.state.searchParams;
|
||||||
|
let page, sortDir;
|
||||||
|
if (sortKey === searchParams.sort_key) {
|
||||||
|
page = searchParams.page;
|
||||||
|
sortDir = searchParams.sort_dir === "Asc" ? "Desc" : "Asc";
|
||||||
|
} else {
|
||||||
|
page = 1;
|
||||||
|
sortDir = "Desc";
|
||||||
|
}
|
||||||
|
return `/#/search/${searchParams.type_}/${searchParams.q}/${page}/${sortKey}/${sortDir}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortLinkIcon(sortKey) {
|
||||||
|
const searchParams = this.state.searchParams;
|
||||||
|
|
||||||
|
if (searchParams.sort_key !== sortKey) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchParams.sort_dir === "Asc") {
|
||||||
|
return (
|
||||||
|
<svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
torrentsTable() {
|
torrentsTable() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -151,19 +162,19 @@ export class Search extends Component<any, State> {
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="search-name-col">
|
<th class="search-name-col">
|
||||||
<SortableLink sortKey="Name" state={this.state}/>
|
{this.sortLink("Name")}
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right">
|
<th class="text-right">
|
||||||
<SortableLink sortKey="Size" state={this.state}/>
|
{this.sortLink("Size")}
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right">
|
<th class="text-right">
|
||||||
<SortableLink sortKey="Seeders" state={this.state}/>
|
{this.sortLink("Seeders")}
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right d-none d-md-table-cell">
|
<th class="text-right d-none d-md-table-cell">
|
||||||
<SortableLink sortKey="Leechers" state={this.state}/>
|
{this.sortLink("Leechers")}
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right d-none d-md-table-cell">
|
<th class="text-right d-none d-md-table-cell">
|
||||||
<SortableLink sortKey="Scraped" state={this.state}/>
|
{this.sortLink("Scraped")}
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Loading…
Reference in New Issue