You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mediarepo/mediarepo-daemon/mediarepo-socket/src/namespaces/files/sorting.rs

193 lines
6.2 KiB
Rust

use std::cmp::Ordering;
use std::collections::HashMap;
use std::iter::FromIterator;
use chrono::NaiveDateTime;
use compare::Compare;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use mediarepo_core::error::RepoResult;
use mediarepo_core::mediarepo_api::types::filtering::{SortDirection, SortKey};
use mediarepo_database::queries::tags::get_content_descriptors_with_tag_count;
use mediarepo_logic::dao::repo::Repo;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{FileDto, FileMetadataDto};
pub struct FileSortContext {
name: Option<String>,
size: u64,
mime_type: String,
namespaces: HashMap<String, Vec<String>>,
tag_count: u32,
import_time: NaiveDateTime,
create_time: NaiveDateTime,
change_time: NaiveDateTime,
}
#[tracing::instrument(level = "debug", skip(repo, files))]
pub async fn sort_files_by_properties(
repo: &Repo,
sort_expression: Vec<SortKey>,
files: &mut Vec<FileDto>,
) -> RepoResult<()> {
let contexts = build_sort_context(repo, files).await?;
files.sort_by(|a, b| {
compare_files(
contexts.get(&a.id()).unwrap(),
contexts.get(&b.id()).unwrap(),
&sort_expression,
)
});
Ok(())
}
async fn build_sort_context(
repo: &Repo,
files: &Vec<FileDto>,
) -> RepoResult<HashMap<i64, FileSortContext>> {
let cd_ids: Vec<i64> = files.par_iter().map(|f| f.cd_id()).collect();
let file_ids: Vec<i64> = files.par_iter().map(|f| f.id()).collect();
let mut cid_nsp: HashMap<i64, HashMap<String, Vec<String>>> = repo
.tag()
.cdids_with_namespaced_tags(cd_ids.clone())
.await?;
let mut cid_tag_counts = get_content_descriptors_with_tag_count(repo.db(), cd_ids).await?;
let files_metadata = repo.file().all_metadata(file_ids).await?;
let mut file_metadata_map: HashMap<i64, FileMetadataDto> =
HashMap::from_iter(files_metadata.into_iter().map(|m| (m.file_id(), m)));
let mut contexts = HashMap::new();
for file in files {
if let Some(metadata) = file_metadata_map.remove(&file.id()) {
let context = FileSortContext {
name: metadata.name().cloned(),
size: metadata.size() as u64,
mime_type: file.mime_type().to_owned(),
namespaces: cid_nsp
.remove(&file.cd_id())
.unwrap_or_else(|| HashMap::with_capacity(0)),
tag_count: cid_tag_counts.remove(&file.cd_id()).unwrap_or(0),
import_time: metadata.import_time().to_owned(),
create_time: metadata.import_time().to_owned(),
change_time: metadata.change_time().to_owned(),
};
contexts.insert(file.id(), context);
}
}
Ok(contexts)
}
#[tracing::instrument(level = "trace", skip_all)]
fn compare_files(
ctx_a: &FileSortContext,
ctx_b: &FileSortContext,
expression: &Vec<SortKey>,
) -> Ordering {
let cmp_date = compare::natural();
let cmp_u64 = compare::natural();
let cmp_u32 = compare::natural();
for sort_key in expression {
let ordering = match sort_key {
SortKey::Namespace(namespace) => {
let list_a = ctx_a.namespaces.get(&namespace.name);
let list_b = ctx_b.namespaces.get(&namespace.name);
let cmp_result = if let (Some(list_a), Some(list_b)) = (list_a, list_b) {
compare_tag_lists(list_a, list_b)
} else if list_a.is_some() {
Ordering::Greater
} else if list_b.is_some() {
Ordering::Less
} else {
Ordering::Equal
};
adjust_for_dir(cmp_result, &namespace.direction)
}
SortKey::FileName(direction) => {
adjust_for_dir(compare_opts(&ctx_a.name, &ctx_b.name), direction)
}
SortKey::FileSize(direction) => {
adjust_for_dir(cmp_u64.compare(&ctx_a.size, &ctx_b.size), direction)
}
SortKey::FileImportedTime(direction) => adjust_for_dir(
cmp_date.compare(&ctx_a.import_time, &ctx_b.import_time),
direction,
),
SortKey::FileCreatedTime(direction) => adjust_for_dir(
cmp_date.compare(&ctx_a.create_time, &ctx_b.create_time),
direction,
),
SortKey::FileChangeTime(direction) => adjust_for_dir(
cmp_date.compare(&ctx_a.change_time, &ctx_b.change_time),
direction,
),
SortKey::FileType(direction) => {
adjust_for_dir(ctx_a.mime_type.cmp(&ctx_b.mime_type), direction)
}
SortKey::NumTags(direction) => adjust_for_dir(
cmp_u32.compare(&ctx_a.tag_count, &ctx_b.tag_count),
direction,
),
};
if !ordering.is_eq() {
return ordering;
}
}
Ordering::Equal
}
fn compare_opts<T: Ord + Sized>(opt_a: &Option<T>, opt_b: &Option<T>) -> Ordering {
let cmp = compare::natural();
if let (Some(a), Some(b)) = (opt_a, opt_b) {
cmp.compare(a, b)
} else if opt_a.is_some() {
Ordering::Greater
} else if opt_b.is_some() {
Ordering::Less
} else {
Ordering::Equal
}
}
fn compare_f32(a: f32, b: f32) -> Ordering {
if a > b {
Ordering::Greater
} else if b > a {
Ordering::Less
} else {
Ordering::Equal
}
}
fn adjust_for_dir(ordering: Ordering, direction: &SortDirection) -> Ordering {
if *direction == SortDirection::Descending {
ordering.reverse()
} else {
ordering
}
}
fn compare_tag_lists(list_a: &[String], list_b: &[String]) -> Ordering {
let first_diff = list_a.iter().zip(list_b.iter()).find(|(a, b)| *a != *b);
if let Some(diff) = first_diff {
if let (Some(num_a), Some(num_b)) = (diff.0.parse::<f32>().ok(), diff.1.parse::<f32>().ok())
{
compare_f32(num_a, num_b)
} else {
let cmp = compare::natural();
cmp.compare(diff.0, diff.1)
}
} else {
Ordering::Equal
}
}