use crate::clip::Clip;
use crate::logging::Rating;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Default)]
pub struct FilterCriteria {
pub rating: Option<RatingFilter>,
pub is_favorite: Option<bool>,
pub is_rejected: Option<bool>,
pub keywords: Vec<String>,
pub date_range: Option<DateRange>,
pub duration_range: Option<DurationRange>,
pub file_extension: Option<String>,
pub has_markers: Option<bool>,
}
#[derive(Debug, Clone, Copy)]
pub enum RatingFilter {
Exact(Rating),
Minimum(Rating),
Maximum(Rating),
Range(Rating, Rating),
}
#[derive(Debug, Clone, Copy)]
pub struct DateRange {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy)]
pub struct DurationRange {
pub min: i64,
pub max: i64,
}
#[derive(Debug, Clone, Default)]
pub struct ClipFilter {
criteria: FilterCriteria,
}
impl ClipFilter {
#[must_use]
pub fn new() -> Self {
Self {
criteria: FilterCriteria::default(),
}
}
#[must_use]
pub fn with_rating(mut self, rating: RatingFilter) -> Self {
self.criteria.rating = Some(rating);
self
}
#[must_use]
pub fn with_favorite(mut self, is_favorite: bool) -> Self {
self.criteria.is_favorite = Some(is_favorite);
self
}
#[must_use]
pub fn with_rejected(mut self, is_rejected: bool) -> Self {
self.criteria.is_rejected = Some(is_rejected);
self
}
#[must_use]
pub fn with_keyword(mut self, keyword: impl Into<String>) -> Self {
self.criteria.keywords.push(keyword.into());
self
}
#[must_use]
pub fn with_date_range(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
self.criteria.date_range = Some(DateRange { start, end });
self
}
#[must_use]
pub fn with_duration_range(mut self, min: i64, max: i64) -> Self {
self.criteria.duration_range = Some(DurationRange { min, max });
self
}
#[must_use]
pub fn with_extension(mut self, extension: impl Into<String>) -> Self {
self.criteria.file_extension = Some(extension.into());
self
}
#[must_use]
pub fn with_has_markers(mut self, has_markers: bool) -> Self {
self.criteria.has_markers = Some(has_markers);
self
}
#[must_use]
pub fn apply<'a>(&self, clips: &'a [Clip]) -> Vec<&'a Clip> {
clips.iter().filter(|clip| self.matches(clip)).collect()
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn matches(&self, clip: &Clip) -> bool {
if let Some(rating_filter) = self.criteria.rating {
match rating_filter {
RatingFilter::Exact(rating) => {
if clip.rating != rating {
return false;
}
}
RatingFilter::Minimum(min) => {
if clip.rating < min {
return false;
}
}
RatingFilter::Maximum(max) => {
if clip.rating > max {
return false;
}
}
RatingFilter::Range(min, max) => {
if clip.rating < min || clip.rating > max {
return false;
}
}
}
}
if let Some(is_favorite) = self.criteria.is_favorite {
if clip.is_favorite != is_favorite {
return false;
}
}
if let Some(is_rejected) = self.criteria.is_rejected {
if clip.is_rejected != is_rejected {
return false;
}
}
if !self.criteria.keywords.is_empty() {
let has_all_keywords = self
.criteria
.keywords
.iter()
.all(|kw| clip.keywords.contains(kw));
if !has_all_keywords {
return false;
}
}
if let Some(date_range) = self.criteria.date_range {
if clip.created_at < date_range.start || clip.created_at > date_range.end {
return false;
}
}
if let Some(duration_range) = self.criteria.duration_range {
if let Some(duration) = clip.effective_duration() {
if duration < duration_range.min || duration > duration_range.max {
return false;
}
} else {
return false;
}
}
if let Some(ext) = &self.criteria.file_extension {
if let Some(clip_ext) = clip.file_path.extension() {
if clip_ext.to_string_lossy().to_lowercase() != ext.to_lowercase() {
return false;
}
} else {
return false;
}
}
if let Some(has_markers) = self.criteria.has_markers {
if clip.markers.is_empty() == has_markers {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_rating_filter() {
let mut clip1 = Clip::new(PathBuf::from("/test1.mov"));
clip1.set_rating(Rating::FiveStars);
let mut clip2 = Clip::new(PathBuf::from("/test2.mov"));
clip2.set_rating(Rating::ThreeStars);
let clips = vec![clip1, clip2];
let filter = ClipFilter::new().with_rating(RatingFilter::Minimum(Rating::FourStars));
let results = filter.apply(&clips);
assert_eq!(results.len(), 1);
assert_eq!(results[0].rating, Rating::FiveStars);
}
#[test]
fn test_favorite_filter() {
let mut clip1 = Clip::new(PathBuf::from("/test1.mov"));
clip1.set_favorite(true);
let clip2 = Clip::new(PathBuf::from("/test2.mov"));
let clips = vec![clip1, clip2];
let filter = ClipFilter::new().with_favorite(true);
let results = filter.apply(&clips);
assert_eq!(results.len(), 1);
assert!(results[0].is_favorite);
}
#[test]
fn test_keyword_filter() {
let mut clip1 = Clip::new(PathBuf::from("/test1.mov"));
clip1.add_keyword("interview");
clip1.add_keyword("john");
let mut clip2 = Clip::new(PathBuf::from("/test2.mov"));
clip2.add_keyword("interview");
let clips = vec![clip1, clip2];
let filter = ClipFilter::new()
.with_keyword("interview")
.with_keyword("john");
let results = filter.apply(&clips);
assert_eq!(results.len(), 1);
}
}