Skip to content

Commit

Permalink
feat: added filtering torrents on tags
Browse files Browse the repository at this point in the history
  • Loading branch information
mickvandijke committed Jun 1, 2023
1 parent a1bd92f commit 4286ba9
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/databases/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum Error {
UserNotFound,
CategoryAlreadyExists,
CategoryNotFound,
TagAlreadyExists,
TagNotFound,
TorrentNotFound,
TorrentAlreadyExists, // when uploading an already uploaded info_hash
TorrentTitleAlreadyExists,
Expand Down Expand Up @@ -158,6 +160,7 @@ pub trait Database: Sync + Send {
&self,
search: &Option<String>,
categories: &Option<Vec<String>>,
tags: &Option<Vec<String>>,
sort: &Sorting,
offset: u64,
page_size: u8,
Expand Down Expand Up @@ -248,6 +251,9 @@ pub trait Database: Sync + Send {
/// Remove all tags from torrent.
async fn delete_all_torrent_tag_links(&self, torrent_id: i64) -> Result<(), Error>;

/// Get tag from name.
async fn get_tag_from_name(&self, name: &str) -> Result<TorrentTag, Error>;

/// Get all tags as `Vec<TorrentTag>`.
async fn get_tags(&self) -> Result<Vec<TorrentTag>, Error>;

Expand Down
36 changes: 35 additions & 1 deletion src/databases/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ impl Database for Mysql {
&self,
search: &Option<String>,
categories: &Option<Vec<String>>,
tags: &Option<Vec<String>>,
sort: &Sorting,
offset: u64,
limit: u8,
Expand Down Expand Up @@ -338,11 +339,36 @@ impl Database for Mysql {
String::new()
};

let tag_filter_query = if let Some(t) = tags {
let mut i = 0;
let mut tag_filters = String::new();
for tag in t.iter() {
// don't take user input in the db query
if let Ok(sanitized_tag) = self.get_tag_from_name(tag).await {
let mut str = format!("tl.tag_id = '{}'", sanitized_tag.tag_id);
if i > 0 {
str = format!(" OR {str}");
}
tag_filters.push_str(&str);
i += 1;
}
}
if tag_filters.is_empty() {
String::new()
} else {
format!("INNER JOIN torrust_torrent_tag_links tl ON tt.torrent_id = tl.torrent_id AND ({tag_filters}) ")
}
} else {
String::new()
};

let mut query_string = format!(
"SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, tt.size AS file_size,
CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders,
CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers
FROM torrust_torrents tt {category_filter_query}
FROM torrust_torrents tt
{category_filter_query}
{tag_filter_query}
INNER JOIN torrust_user_profiles tp ON tt.uploader_id = tp.user_id
INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id
LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id
Expand Down Expand Up @@ -736,6 +762,14 @@ impl Database for Mysql {
.map_err(|err| database::Error::ErrorWithText(err.to_string()))
}

async fn get_tag_from_name(&self, name: &str) -> Result<TorrentTag, database::Error> {
query_as::<_, TorrentTag>("SELECT tag_id, name FROM torrust_torrent_tags WHERE name = ?")
.bind(name)
.fetch_one(&self.pool)
.await
.map_err(|err| database::Error::TagNotFound)
}

async fn get_tags(&self) -> Result<Vec<TorrentTag>, database::Error> {
query_as::<_, TorrentTag>(
"SELECT tag_id, name FROM torrust_torrent_tags"
Expand Down
38 changes: 36 additions & 2 deletions src/databases/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ impl Database for Sqlite {
&self,
search: &Option<String>,
categories: &Option<Vec<String>>,
tags: &Option<Vec<String>>,
sort: &Sorting,
offset: u64,
limit: u8,
Expand Down Expand Up @@ -328,11 +329,36 @@ impl Database for Sqlite {
String::new()
};

let tag_filter_query = if let Some(t) = tags {
let mut i = 0;
let mut tag_filters = String::new();
for tag in t.iter() {
// don't take user input in the db query
if let Ok(sanitized_tag) = self.get_tag_from_name(tag).await {
let mut str = format!("tl.tag_id = '{}'", sanitized_tag.tag_id);
if i > 0 {
str = format!(" OR {str}");
}
tag_filters.push_str(&str);
i += 1;
}
}
if tag_filters.is_empty() {
String::new()
} else {
format!("INNER JOIN torrust_torrent_tag_links tl ON tt.torrent_id = tl.torrent_id AND ({tag_filters}) ")
}
} else {
String::new()
};

let mut query_string = format!(
"SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, tt.date_uploaded, tt.size AS file_size,
"SELECT tt.torrent_id, tp.username AS uploader, tt.info_hash, ti.title, ti.description, tt.category_id, DATE_FORMAT(tt.date_uploaded, '%Y-%m-%d %H:%i:%s') AS date_uploaded, tt.size AS file_size,
CAST(COALESCE(sum(ts.seeders),0) as signed) as seeders,
CAST(COALESCE(sum(ts.leechers),0) as signed) as leechers
FROM torrust_torrents tt {category_filter_query}
FROM torrust_torrents tt
{category_filter_query}
{tag_filter_query}
INNER JOIN torrust_user_profiles tp ON tt.uploader_id = tp.user_id
INNER JOIN torrust_torrent_info ti ON tt.torrent_id = ti.torrent_id
LEFT JOIN torrust_torrent_tracker_stats ts ON tt.torrent_id = ts.torrent_id
Expand Down Expand Up @@ -726,6 +752,14 @@ impl Database for Sqlite {
.map_err(|err| database::Error::ErrorWithText(err.to_string()))
}

async fn get_tag_from_name(&self, name: &str) -> Result<TorrentTag, database::Error> {
query_as::<_, TorrentTag>("SELECT tag_id, name FROM torrust_torrent_tags WHERE name = ?")
.bind(name)
.fetch_one(&self.pool)
.await
.map_err(|err| database::Error::TagNotFound)
}

async fn get_tags(&self) -> Result<Vec<TorrentTag>, database::Error> {
query_as::<_, TorrentTag>(
"SELECT tag_id, name FROM torrust_torrent_tags"
Expand Down
10 changes: 10 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ pub enum ServiceError {
#[display(fmt = "Selected category does not exist.")]
InvalidCategory,

#[display(fmt = "Selected tag does not exist.")]
InvalidTag,

#[display(fmt = "Unauthorized action.")]
Unauthorized,

Expand All @@ -123,6 +126,9 @@ pub enum ServiceError {
#[display(fmt = "Category already exists.")]
CategoryExists,

#[display(fmt = "Tag already exists.")]
TagExists,

#[display(fmt = "Category not found.")]
CategoryNotFound,

Expand Down Expand Up @@ -165,11 +171,13 @@ impl ResponseError for ServiceError {
ServiceError::InvalidFileType => StatusCode::BAD_REQUEST,
ServiceError::BadRequest => StatusCode::BAD_REQUEST,
ServiceError::InvalidCategory => StatusCode::BAD_REQUEST,
ServiceError::InvalidTag => StatusCode::BAD_REQUEST,
ServiceError::Unauthorized => StatusCode::FORBIDDEN,
ServiceError::InfoHashAlreadyExists => StatusCode::BAD_REQUEST,
ServiceError::TorrentTitleAlreadyExists => StatusCode::BAD_REQUEST,
ServiceError::TrackerOffline => StatusCode::INTERNAL_SERVER_ERROR,
ServiceError::CategoryExists => StatusCode::BAD_REQUEST,
ServiceError::TagExists => StatusCode::BAD_REQUEST,
ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
ServiceError::EmailMissing => StatusCode::NOT_FOUND,
ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR,
Expand Down Expand Up @@ -217,6 +225,8 @@ impl From<database::Error> for ServiceError {
database::Error::UserNotFound => ServiceError::UserNotFound,
database::Error::CategoryAlreadyExists => ServiceError::CategoryExists,
database::Error::CategoryNotFound => ServiceError::InvalidCategory,
database::Error::TagAlreadyExists => ServiceError::TagExists,
database::Error::TagNotFound => ServiceError::InvalidTag,
database::Error::TorrentNotFound => ServiceError::TorrentNotFound,
database::Error::TorrentAlreadyExists => ServiceError::InfoHashAlreadyExists,
database::Error::TorrentTitleAlreadyExists => ServiceError::TorrentTitleAlreadyExists,
Expand Down
7 changes: 7 additions & 0 deletions src/services/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub struct ListingRequest {
pub sort: Option<Sorting>,
/// Expects comma separated string, eg: "?categories=movie,other,app"
pub categories: Option<String>,
/// Expects comma separated string, eg: "?tags=Linux,Ubuntu"
pub tags: Option<String>,
pub search: Option<String>,
}

Expand All @@ -46,6 +48,7 @@ pub struct ListingRequest {
pub struct ListingSpecification {
pub search: Option<String>,
pub categories: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
pub sort: Sorting,
pub offset: u64,
pub page_size: u8,
Expand Down Expand Up @@ -325,9 +328,12 @@ impl Index {

let categories = request.categories.as_csv::<String>().unwrap_or(None);

let tags = request.tags.as_csv::<String>().unwrap_or(None);

ListingSpecification {
search: request.search.clone(),
categories,
tags,
sort,
offset,
page_size,
Expand Down Expand Up @@ -655,6 +661,7 @@ impl DbTorrentListingGenerator {
.get_torrents_search_sorted_paginated(
&specification.search,
&specification.categories,
&specification.tags,
&specification.sort,
specification.offset,
specification.page_size,
Expand Down

0 comments on commit 4286ba9

Please sign in to comment.