feature/sort-by-tag-count #32
@@ -106,6 +106,8 @@ pub enum SortType {
|
|||||||
Shuffle,
|
Shuffle,
|
||||||
NameAsc,
|
NameAsc,
|
||||||
NameDesc,
|
NameDesc,
|
||||||
|
TagCountAsc,
|
||||||
|
TagCountDesc,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
66
src/files.rs
66
src/files.rs
@@ -19,9 +19,9 @@ use log::{debug, error, info, trace};
|
|||||||
use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse, SortType};
|
use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse, SortType};
|
||||||
use crate::{create_thumbnails, AppState};
|
use crate::{create_thumbnails, AppState};
|
||||||
|
|
||||||
use crate::data::SortType::{NameAsc};
|
use crate::data::SortType::NameAsc;
|
||||||
use crate::error::IntoHttpError;
|
use crate::error::IntoHttpError;
|
||||||
use crate::tags::TagDao;
|
use crate::tags::{FileWithTagCount, TagDao};
|
||||||
use crate::video::StreamActor;
|
use crate::video::StreamActor;
|
||||||
use path_absolutize::*;
|
use path_absolutize::*;
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
@@ -68,6 +68,13 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
"Failed to get files with tag_ids: {:?} with filter_mode: {:?}",
|
"Failed to get files with tag_ids: {:?} with filter_mode: {:?}",
|
||||||
tag_ids, filter_mode
|
tag_ids, filter_mode
|
||||||
))
|
))
|
||||||
|
.inspect(|files| {
|
||||||
|
debug!(
|
||||||
|
"Found {:?} tagged files, filtering down by search path {:?}",
|
||||||
|
files.len(),
|
||||||
|
search_path
|
||||||
|
)
|
||||||
|
})
|
||||||
.map(|tagged_files| {
|
.map(|tagged_files| {
|
||||||
tagged_files
|
tagged_files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -77,12 +84,12 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
f.starts_with(&format!(
|
f.file_name.starts_with(&format!(
|
||||||
"{}/",
|
"{}/",
|
||||||
search_path.strip_suffix('/').unwrap_or_else(|| search_path)
|
search_path.strip_suffix('/').unwrap_or_else(|| search_path)
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<FileWithTagCount>>()
|
||||||
})
|
})
|
||||||
.map(|files| sort(files, req.sort.unwrap_or(NameAsc)))
|
.map(|files| sort(files, req.sort.unwrap_or(NameAsc)))
|
||||||
.inspect(|files| debug!("Found {:?} files", files.len()))
|
.inspect(|files| debug!("Found {:?} files", files.len()))
|
||||||
@@ -106,7 +113,7 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
if let Ok(files) = file_system.get_files_for_path(search_path) {
|
if let Ok(files) = file_system.get_files_for_path(search_path) {
|
||||||
debug!("Valid search path: {:?}", search_path);
|
debug!("Valid search path: {:?}", search_path);
|
||||||
|
|
||||||
let mut photos = files
|
let photos = files
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&f| {
|
.filter(|&f| {
|
||||||
f.metadata().map_or_else(
|
f.metadata().map_or_else(
|
||||||
@@ -122,8 +129,14 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
relative.to_path_buf()
|
relative.to_path_buf()
|
||||||
})
|
})
|
||||||
.map(|f| f.to_str().unwrap().to_string())
|
.map(|f| f.to_str().unwrap().to_string())
|
||||||
.filter(|file_path| {
|
.map(|file_name| {
|
||||||
if let (Some(tag_ids), Ok(mut tag_dao)) = (&req.tag_ids, tag_dao.lock()) {
|
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
|
||||||
|
let file_tags = tag_dao.get_tags_for_path(&file_name).unwrap_or_default();
|
||||||
|
|
||||||
|
(file_name, file_tags)
|
||||||
|
})
|
||||||
|
.filter(|(_, file_tags)| {
|
||||||
|
if let Some(tag_ids) = &req.tag_ids {
|
||||||
let tag_ids = tag_ids
|
let tag_ids = tag_ids
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter_map(|t| t.parse().ok())
|
.filter_map(|t| t.parse().ok())
|
||||||
@@ -138,7 +151,6 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
.collect::<Vec<i32>>();
|
.collect::<Vec<i32>>();
|
||||||
|
|
||||||
let filter_mode = &req.tag_filter_mode.unwrap_or(FilterMode::Any);
|
let filter_mode = &req.tag_filter_mode.unwrap_or(FilterMode::Any);
|
||||||
let file_tags = tag_dao.get_tags_for_path(file_path).unwrap_or_default();
|
|
||||||
let excluded = file_tags.iter().any(|t| excluded_tag_ids.contains(&t.id));
|
let excluded = file_tags.iter().any(|t| excluded_tag_ids.contains(&t.id));
|
||||||
|
|
||||||
return !excluded
|
return !excluded
|
||||||
@@ -152,11 +164,20 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
|
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>();
|
.map(|(file_name, tags)| FileWithTagCount {
|
||||||
|
file_name,
|
||||||
|
tag_count: tags.len() as i64,
|
||||||
|
})
|
||||||
|
.collect::<Vec<FileWithTagCount>>();
|
||||||
|
|
||||||
|
let mut response_files = photos
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| f.file_name)
|
||||||
|
.collect::<Vec<String>>();
|
||||||
if let Some(sort_type) = req.sort {
|
if let Some(sort_type) = req.sort {
|
||||||
debug!("Sorting files: {:?}", sort_type);
|
debug!("Sorting files: {:?}", sort_type);
|
||||||
photos = sort(photos, sort_type)
|
response_files = sort(photos, sort_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dirs = files
|
let dirs = files
|
||||||
@@ -169,25 +190,37 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
.map(|f| f.to_str().unwrap().to_string())
|
.map(|f| f.to_str().unwrap().to_string())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
HttpResponse::Ok().json(PhotosResponse { photos, dirs })
|
HttpResponse::Ok().json(PhotosResponse {
|
||||||
|
photos: response_files,
|
||||||
|
dirs,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
error!("Bad photos request: {}", req.path);
|
error!("Bad photos request: {}", req.path);
|
||||||
HttpResponse::BadRequest().finish()
|
HttpResponse::BadRequest().finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort(mut files: Vec<String>, sort_type: SortType) -> Vec<String> {
|
fn sort(mut files: Vec<FileWithTagCount>, sort_type: SortType) -> Vec<String> {
|
||||||
match sort_type {
|
match sort_type {
|
||||||
SortType::Shuffle => files.shuffle(&mut thread_rng()),
|
SortType::Shuffle => files.shuffle(&mut thread_rng()),
|
||||||
SortType::NameAsc => {
|
SortType::NameAsc => {
|
||||||
files.sort();
|
files.sort_by(|l, r| l.file_name.cmp(&r.file_name));
|
||||||
}
|
}
|
||||||
SortType::NameDesc => {
|
SortType::NameDesc => {
|
||||||
files.sort_by(|l, r| r.cmp(l));
|
files.sort_by(|l, r| r.file_name.cmp(&l.file_name));
|
||||||
|
}
|
||||||
|
SortType::TagCountAsc => {
|
||||||
|
files.sort_by(|l, r| l.tag_count.cmp(&r.tag_count));
|
||||||
|
}
|
||||||
|
SortType::TagCountDesc => {
|
||||||
|
files.sort_by(|l, r| r.tag_count.cmp(&l.tag_count));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
files
|
files
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.file_name.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_files(dir: &Path) -> io::Result<Vec<PathBuf>> {
|
pub fn list_files(dir: &Path) -> io::Result<Vec<PathBuf>> {
|
||||||
@@ -396,10 +429,7 @@ mod tests {
|
|||||||
if self.err {
|
if self.err {
|
||||||
Err(anyhow!("Error for test"))
|
Err(anyhow!("Error for test"))
|
||||||
} else if let Some(files) = self.files.get(path) {
|
} else if let Some(files) = self.files.get(path) {
|
||||||
Ok(files
|
Ok(files.iter().map(PathBuf::from).collect::<Vec<PathBuf>>())
|
||||||
.iter()
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.collect::<Vec<PathBuf>>())
|
|
||||||
} else {
|
} else {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/main.rs
25
src/main.rs
@@ -184,8 +184,10 @@ async fn upload_image(
|
|||||||
&full_path.to_str().unwrap().to_string(),
|
&full_path.to_str().unwrap().to_string(),
|
||||||
true,
|
true,
|
||||||
) {
|
) {
|
||||||
let context = opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
|
let context =
|
||||||
tracer.span_builder("file write")
|
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
|
||||||
|
tracer
|
||||||
|
.span_builder("file write")
|
||||||
.start_with_context(&tracer, &context);
|
.start_with_context(&tracer, &context);
|
||||||
|
|
||||||
if !full_path.is_file() && is_image_or_video(&full_path) {
|
if !full_path.is_file() && is_image_or_video(&full_path) {
|
||||||
@@ -322,7 +324,10 @@ async fn get_video_part(
|
|||||||
file.into_response(&request)
|
file.into_response(&request)
|
||||||
} else {
|
} else {
|
||||||
error!("Video part not found: {:?}", file_part);
|
error!("Video part not found: {:?}", file_part);
|
||||||
span.set_status(Status::error(format!("Video part not found '{}'", file_part.to_str().unwrap())));
|
span.set_status(Status::error(format!(
|
||||||
|
"Video part not found '{}'",
|
||||||
|
file_part.to_str().unwrap()
|
||||||
|
)));
|
||||||
HttpResponse::NotFound().finish()
|
HttpResponse::NotFound().finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,7 +459,8 @@ fn create_thumbnails() {
|
|||||||
|
|
||||||
let mut video_span = tracer.start_with_context(
|
let mut video_span = tracer.start_with_context(
|
||||||
"generate_video_thumbnail",
|
"generate_video_thumbnail",
|
||||||
&opentelemetry::Context::new().with_remote_span_context(span.span_context().clone()),
|
&opentelemetry::Context::new()
|
||||||
|
.with_remote_span_context(span.span_context().clone()),
|
||||||
);
|
);
|
||||||
video_span.set_attributes(vec![
|
video_span.set_attributes(vec![
|
||||||
KeyValue::new("type", "video"),
|
KeyValue::new("type", "video"),
|
||||||
@@ -538,7 +544,6 @@ fn main() -> std::io::Result<()> {
|
|||||||
if let Err(err) = dotenv::dotenv() {
|
if let Err(err) = dotenv::dotenv() {
|
||||||
println!("Error parsing .env {:?}", err);
|
println!("Error parsing .env {:?}", err);
|
||||||
}
|
}
|
||||||
// env_logger::init();
|
|
||||||
|
|
||||||
run_migrations(&mut connect()).expect("Failed to run migrations");
|
run_migrations(&mut connect()).expect("Failed to run migrations");
|
||||||
|
|
||||||
@@ -546,9 +551,17 @@ fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
let system = actix::System::new();
|
let system = actix::System::new();
|
||||||
system.block_on(async {
|
system.block_on(async {
|
||||||
|
// Just use basic logger when running a non-release build
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
env_logger::init();
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
otel::init_logs();
|
otel::init_logs();
|
||||||
otel::init_tracing();
|
otel::init_tracing();
|
||||||
|
}
|
||||||
|
|
||||||
create_thumbnails();
|
create_thumbnails();
|
||||||
|
|
||||||
let app_data = Data::new(AppState::default());
|
let app_data = Data::new(AppState::default());
|
||||||
|
|||||||
54
src/tags.rs
54
src/tags.rs
@@ -202,12 +202,12 @@ pub trait TagDao {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
) -> anyhow::Result<Vec<String>>;
|
) -> anyhow::Result<Vec<FileWithTagCount>>;
|
||||||
fn get_files_with_any_tag_ids(
|
fn get_files_with_any_tag_ids(
|
||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
) -> anyhow::Result<Vec<String>>;
|
) -> anyhow::Result<Vec<FileWithTagCount>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SqliteTagDao {
|
pub struct SqliteTagDao {
|
||||||
@@ -277,7 +277,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
.and_then(|_| {
|
.and_then(|_| {
|
||||||
info!("Inserted tag: {:?}", name);
|
info!("Inserted tag: {:?}", name);
|
||||||
define_sql_function! {
|
define_sql_function! {
|
||||||
fn last_insert_rowid() -> diesel::sql_types::Integer;
|
fn last_insert_rowid() -> Integer;
|
||||||
}
|
}
|
||||||
diesel::select(last_insert_rowid())
|
diesel::select(last_insert_rowid())
|
||||||
.get_result::<i32>(&mut self.connection)
|
.get_result::<i32>(&mut self.connection)
|
||||||
@@ -353,7 +353,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
) -> anyhow::Result<Vec<String>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
use diesel::dsl::*;
|
use diesel::dsl::*;
|
||||||
|
|
||||||
let exclude_subquery = tagged_photo::table
|
let exclude_subquery = tagged_photo::table
|
||||||
@@ -365,10 +365,21 @@ impl TagDao for SqliteTagDao {
|
|||||||
.filter(tagged_photo::tag_id.eq_any(tag_ids.clone()))
|
.filter(tagged_photo::tag_id.eq_any(tag_ids.clone()))
|
||||||
.filter(tagged_photo::photo_name.ne_all(exclude_subquery))
|
.filter(tagged_photo::photo_name.ne_all(exclude_subquery))
|
||||||
.group_by(tagged_photo::photo_name)
|
.group_by(tagged_photo::photo_name)
|
||||||
.select((tagged_photo::photo_name, count(tagged_photo::tag_id)))
|
.select((
|
||||||
|
tagged_photo::photo_name,
|
||||||
|
count_distinct(tagged_photo::tag_id),
|
||||||
|
))
|
||||||
.having(count_distinct(tagged_photo::tag_id).ge(tag_ids.len() as i64))
|
.having(count_distinct(tagged_photo::tag_id).ge(tag_ids.len() as i64))
|
||||||
.select(tagged_photo::photo_name)
|
.get_results::<(String, i64)>(&mut self.connection)
|
||||||
.get_results::<String>(&mut self.connection)
|
.map(|results| {
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(file_name, tag_count)| FileWithTagCount {
|
||||||
|
file_name,
|
||||||
|
tag_count,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids))
|
.with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +387,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
) -> anyhow::Result<Vec<String>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
use diesel::dsl::*;
|
use diesel::dsl::*;
|
||||||
|
|
||||||
let exclude_subquery = tagged_photo::table
|
let exclude_subquery = tagged_photo::table
|
||||||
@@ -388,9 +399,20 @@ impl TagDao for SqliteTagDao {
|
|||||||
.filter(tagged_photo::tag_id.eq_any(tag_ids.clone()))
|
.filter(tagged_photo::tag_id.eq_any(tag_ids.clone()))
|
||||||
.filter(tagged_photo::photo_name.ne_all(exclude_subquery))
|
.filter(tagged_photo::photo_name.ne_all(exclude_subquery))
|
||||||
.group_by(tagged_photo::photo_name)
|
.group_by(tagged_photo::photo_name)
|
||||||
.select((tagged_photo::photo_name, count(tagged_photo::tag_id)))
|
.select((
|
||||||
.select(tagged_photo::photo_name)
|
tagged_photo::photo_name,
|
||||||
.get_results::<String>(&mut self.connection)
|
count_distinct(tagged_photo::tag_id),
|
||||||
|
))
|
||||||
|
.get_results::<(String, i64)>(&mut self.connection)
|
||||||
|
.map(|results| {
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(file_name, tag_count)| FileWithTagCount {
|
||||||
|
file_name,
|
||||||
|
tag_count,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids))
|
.with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,7 +539,7 @@ mod tests {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
_exclude_tag_ids: Vec<i32>,
|
_exclude_tag_ids: Vec<i32>,
|
||||||
) -> anyhow::Result<Vec<String>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +547,7 @@ mod tests {
|
|||||||
&mut self,
|
&mut self,
|
||||||
_tag_ids: Vec<i32>,
|
_tag_ids: Vec<i32>,
|
||||||
_exclude_tag_ids: Vec<i32>,
|
_exclude_tag_ids: Vec<i32>,
|
||||||
) -> anyhow::Result<Vec<String>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,3 +629,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileWithTagCount {
|
||||||
|
pub file_name: String,
|
||||||
|
pub tag_count: i64,
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ impl UserDao for TestUserDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn user_exists(&mut self, user: &str) -> bool {
|
fn user_exists(&mut self, user: &str) -> bool {
|
||||||
self.user_map
|
self.user_map.borrow().iter().any(|u| u.username == user)
|
||||||
.borrow()
|
|
||||||
.iter()
|
|
||||||
.any(|u| u.username == user)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user