#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrganizeCriteria {
ByDate,
ByCamera,
ByScene,
ByCodec,
ByResolution,
ByFrameRate,
ByRating,
ByKeyword,
ByMediaType,
ByExtension,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClipMediaType {
Video,
Audio,
Image,
Graphics,
Unknown,
}
#[derive(Debug, Clone)]
pub struct ClipDescriptor {
pub clip_id: u64,
pub name: String,
pub date: Option<String>,
pub camera: Option<String>,
pub scene: Option<String>,
pub codec: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub frame_rate: Option<f64>,
pub rating: u8,
pub keywords: Vec<String>,
pub media_type: ClipMediaType,
pub extension: Option<String>,
}
impl ClipDescriptor {
#[must_use]
pub fn new(clip_id: u64, name: impl Into<String>) -> Self {
Self {
clip_id,
name: name.into(),
date: None,
camera: None,
scene: None,
codec: None,
width: None,
height: None,
frame_rate: None,
rating: 0,
keywords: Vec::new(),
media_type: ClipMediaType::Unknown,
extension: None,
}
}
#[must_use]
pub fn resolution_label(&self) -> Option<String> {
match (self.width, self.height) {
(Some(w), Some(h)) => Some(format!("{w}x{h}")),
_ => None,
}
}
#[must_use]
pub fn frame_rate_label(&self) -> Option<String> {
self.frame_rate.map(|fps| format!("{fps:.3}"))
}
}
#[derive(Debug, Clone)]
pub struct OrganizedBin {
pub name: String,
pub criteria: OrganizeCriteria,
pub clip_ids: Vec<u64>,
pub sub_bins: Vec<OrganizedBin>,
}
impl OrganizedBin {
#[must_use]
pub fn new(name: impl Into<String>, criteria: OrganizeCriteria) -> Self {
Self {
name: name.into(),
criteria,
clip_ids: Vec::new(),
sub_bins: Vec::new(),
}
}
pub fn add_clip(&mut self, clip_id: u64) {
self.clip_ids.push(clip_id);
}
#[must_use]
pub fn clip_count(&self) -> usize {
self.clip_ids.len()
}
#[must_use]
pub fn total_clip_count(&self) -> usize {
let sub_count: usize = self.sub_bins.iter().map(|b| b.total_clip_count()).sum();
self.clip_ids.len() + sub_count
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.clip_ids.is_empty() && self.sub_bins.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct BinRule {
pub bin_name: String,
pub criteria: OrganizeCriteria,
pub match_value: String,
pub case_sensitive: bool,
}
impl BinRule {
#[must_use]
pub fn new(
bin_name: impl Into<String>,
criteria: OrganizeCriteria,
match_value: impl Into<String>,
) -> Self {
Self {
bin_name: bin_name.into(),
criteria,
match_value: match_value.into(),
case_sensitive: false,
}
}
#[must_use]
pub fn matches(&self, clip: &ClipDescriptor) -> bool {
let clip_value = self.extract_value(clip);
if self.case_sensitive {
clip_value == self.match_value
} else {
clip_value.to_lowercase() == self.match_value.to_lowercase()
}
}
fn extract_value(&self, clip: &ClipDescriptor) -> String {
match self.criteria {
OrganizeCriteria::ByDate => clip.date.clone().unwrap_or_default(),
OrganizeCriteria::ByCamera => clip.camera.clone().unwrap_or_default(),
OrganizeCriteria::ByScene => clip.scene.clone().unwrap_or_default(),
OrganizeCriteria::ByCodec => clip.codec.clone().unwrap_or_default(),
OrganizeCriteria::ByResolution => clip.resolution_label().unwrap_or_default(),
OrganizeCriteria::ByFrameRate => clip.frame_rate_label().unwrap_or_default(),
OrganizeCriteria::ByRating => format!("{}", clip.rating),
OrganizeCriteria::ByKeyword => clip.keywords.join(","),
OrganizeCriteria::ByMediaType => format!("{:?}", clip.media_type),
OrganizeCriteria::ByExtension => clip.extension.clone().unwrap_or_default(),
}
}
}
#[derive(Debug)]
pub struct BinOrganizer {
rules: Vec<BinRule>,
default_criteria: OrganizeCriteria,
}
impl BinOrganizer {
#[must_use]
pub fn new() -> Self {
Self {
rules: Vec::new(),
default_criteria: OrganizeCriteria::ByDate,
}
}
#[must_use]
pub fn with_criteria(criteria: OrganizeCriteria) -> Self {
Self {
rules: Vec::new(),
default_criteria: criteria,
}
}
pub fn add_rule(&mut self, rule: BinRule) {
self.rules.push(rule);
}
#[must_use]
pub fn rule_count(&self) -> usize {
self.rules.len()
}
#[must_use]
pub fn organize(&self, clips: &[ClipDescriptor]) -> Vec<OrganizedBin> {
self.organize_by(clips, self.default_criteria)
}
#[must_use]
pub fn organize_by(
&self,
clips: &[ClipDescriptor],
criteria: OrganizeCriteria,
) -> Vec<OrganizedBin> {
let mut groups: HashMap<String, Vec<u64>> = HashMap::new();
for clip in clips {
let key = self.get_grouping_key(clip, criteria);
groups.entry(key).or_default().push(clip.clip_id);
}
let mut bins: Vec<OrganizedBin> = groups
.into_iter()
.map(|(name, ids)| {
let mut bin = OrganizedBin::new(name, criteria);
bin.clip_ids = ids;
bin
})
.collect();
bins.sort_by(|a, b| a.name.cmp(&b.name));
bins
}
#[must_use]
pub fn apply_rules(&self, clips: &[ClipDescriptor]) -> Vec<OrganizedBin> {
let mut bin_map: HashMap<String, OrganizedBin> = HashMap::new();
for clip in clips {
for rule in &self.rules {
if rule.matches(clip) {
let bin = bin_map
.entry(rule.bin_name.clone())
.or_insert_with(|| OrganizedBin::new(&rule.bin_name, rule.criteria));
bin.add_clip(clip.clip_id);
}
}
}
let mut bins: Vec<OrganizedBin> = bin_map.into_values().collect();
bins.sort_by(|a, b| a.name.cmp(&b.name));
bins
}
fn get_grouping_key(&self, clip: &ClipDescriptor, criteria: OrganizeCriteria) -> String {
match criteria {
OrganizeCriteria::ByDate => clip.date.clone().unwrap_or_else(|| "Unknown Date".into()),
OrganizeCriteria::ByCamera => clip
.camera
.clone()
.unwrap_or_else(|| "Unknown Camera".into()),
OrganizeCriteria::ByScene => clip.scene.clone().unwrap_or_else(|| "No Scene".into()),
OrganizeCriteria::ByCodec => {
clip.codec.clone().unwrap_or_else(|| "Unknown Codec".into())
}
OrganizeCriteria::ByResolution => clip
.resolution_label()
.unwrap_or_else(|| "Unknown Resolution".into()),
OrganizeCriteria::ByFrameRate => clip
.frame_rate_label()
.unwrap_or_else(|| "Unknown FPS".into()),
OrganizeCriteria::ByRating => {
if clip.rating == 0 {
"Unrated".into()
} else {
format!("{} Stars", clip.rating)
}
}
OrganizeCriteria::ByKeyword => {
if clip.keywords.is_empty() {
"No Keywords".into()
} else {
clip.keywords.first().cloned().unwrap_or_default()
}
}
OrganizeCriteria::ByMediaType => format!("{:?}", clip.media_type),
OrganizeCriteria::ByExtension => {
clip.extension.clone().unwrap_or_else(|| "Unknown".into())
}
}
}
}
impl Default for BinOrganizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_clip(id: u64, name: &str) -> ClipDescriptor {
ClipDescriptor::new(id, name)
}
fn make_video_clip(id: u64, name: &str, date: &str, camera: &str) -> ClipDescriptor {
let mut clip = ClipDescriptor::new(id, name);
clip.date = Some(date.to_string());
clip.camera = Some(camera.to_string());
clip.media_type = ClipMediaType::Video;
clip.width = Some(1920);
clip.height = Some(1080);
clip.frame_rate = Some(24.0);
clip.codec = Some("H.264".to_string());
clip.extension = Some("mov".to_string());
clip
}
#[test]
fn test_clip_descriptor_new() {
let clip = make_clip(1, "Test Clip");
assert_eq!(clip.clip_id, 1);
assert_eq!(clip.name, "Test Clip");
assert_eq!(clip.rating, 0);
}
#[test]
fn test_resolution_label() {
let mut clip = make_clip(1, "Clip");
assert!(clip.resolution_label().is_none());
clip.width = Some(1920);
clip.height = Some(1080);
assert_eq!(
clip.resolution_label()
.expect("resolution_label should succeed"),
"1920x1080"
);
}
#[test]
fn test_frame_rate_label() {
let mut clip = make_clip(1, "Clip");
assert!(clip.frame_rate_label().is_none());
clip.frame_rate = Some(23.976);
assert_eq!(
clip.frame_rate_label()
.expect("frame_rate_label should succeed"),
"23.976"
);
}
#[test]
fn test_organized_bin_clip_count() {
let mut bin = OrganizedBin::new("Day 1", OrganizeCriteria::ByDate);
assert_eq!(bin.clip_count(), 0);
assert!(bin.is_empty());
bin.add_clip(1);
bin.add_clip(2);
assert_eq!(bin.clip_count(), 2);
assert!(!bin.is_empty());
}
#[test]
fn test_organized_bin_total_clip_count() {
let mut bin = OrganizedBin::new("Root", OrganizeCriteria::ByDate);
bin.add_clip(1);
let mut sub = OrganizedBin::new("Sub", OrganizeCriteria::ByCamera);
sub.add_clip(2);
sub.add_clip(3);
bin.sub_bins.push(sub);
assert_eq!(bin.total_clip_count(), 3);
}
#[test]
fn test_bin_rule_matches() {
let rule = BinRule::new("Cam A", OrganizeCriteria::ByCamera, "Camera A");
let mut clip = make_clip(1, "Test");
clip.camera = Some("Camera A".to_string());
assert!(rule.matches(&clip));
}
#[test]
fn test_bin_rule_case_insensitive() {
let rule = BinRule::new("Cam A", OrganizeCriteria::ByCamera, "camera a");
let mut clip = make_clip(1, "Test");
clip.camera = Some("Camera A".to_string());
assert!(rule.matches(&clip));
}
#[test]
fn test_bin_rule_no_match() {
let rule = BinRule::new("Cam A", OrganizeCriteria::ByCamera, "Camera B");
let mut clip = make_clip(1, "Test");
clip.camera = Some("Camera A".to_string());
assert!(!rule.matches(&clip));
}
#[test]
fn test_organize_by_date() {
let clips = vec![
make_video_clip(1, "Clip1", "2025-03-01", "Cam A"),
make_video_clip(2, "Clip2", "2025-03-01", "Cam B"),
make_video_clip(3, "Clip3", "2025-03-02", "Cam A"),
];
let organizer = BinOrganizer::with_criteria(OrganizeCriteria::ByDate);
let bins = organizer.organize(&clips);
assert_eq!(bins.len(), 2);
assert_eq!(bins[0].name, "2025-03-01");
assert_eq!(bins[0].clip_count(), 2);
assert_eq!(bins[1].name, "2025-03-02");
assert_eq!(bins[1].clip_count(), 1);
}
#[test]
fn test_organize_by_camera() {
let clips = vec![
make_video_clip(1, "Clip1", "2025-03-01", "Cam A"),
make_video_clip(2, "Clip2", "2025-03-01", "Cam B"),
make_video_clip(3, "Clip3", "2025-03-02", "Cam A"),
];
let organizer = BinOrganizer::with_criteria(OrganizeCriteria::ByCamera);
let bins = organizer.organize(&clips);
assert_eq!(bins.len(), 2);
}
#[test]
fn test_organize_by_resolution() {
let mut clips = vec![
make_video_clip(1, "Clip1", "2025-03-01", "Cam A"),
make_video_clip(2, "Clip2", "2025-03-01", "Cam B"),
];
clips[1].width = Some(1280);
clips[1].height = Some(720);
let organizer = BinOrganizer::with_criteria(OrganizeCriteria::ByResolution);
let bins = organizer.organize(&clips);
assert_eq!(bins.len(), 2);
}
#[test]
fn test_organize_empty() {
let organizer = BinOrganizer::new();
let bins = organizer.organize(&[]);
assert!(bins.is_empty());
}
#[test]
fn test_apply_rules() {
let clips = vec![
make_video_clip(1, "Clip1", "2025-03-01", "Camera A"),
make_video_clip(2, "Clip2", "2025-03-01", "Camera B"),
make_video_clip(3, "Clip3", "2025-03-02", "Camera A"),
];
let mut organizer = BinOrganizer::new();
organizer.add_rule(BinRule::new(
"A Cam",
OrganizeCriteria::ByCamera,
"Camera A",
));
assert_eq!(organizer.rule_count(), 1);
let bins = organizer.apply_rules(&clips);
assert_eq!(bins.len(), 1);
assert_eq!(bins[0].name, "A Cam");
assert_eq!(bins[0].clip_count(), 2);
}
#[test]
fn test_organize_by_rating() {
let mut clips = vec![
make_clip(1, "Clip1"),
make_clip(2, "Clip2"),
make_clip(3, "Clip3"),
];
clips[0].rating = 5;
clips[1].rating = 3;
clips[2].rating = 5;
let organizer = BinOrganizer::with_criteria(OrganizeCriteria::ByRating);
let bins = organizer.organize(&clips);
assert_eq!(bins.len(), 2);
}
}