#![allow(dead_code)]
use crate::clip::ClipId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FavoriteEntry {
pub clip_id: ClipId,
pub added_at: DateTime<Utc>,
pub note: Option<String>,
pub sort_order: i64,
}
impl FavoriteEntry {
fn new(clip_id: ClipId) -> Self {
let now = Utc::now();
Self {
clip_id,
added_at: now,
note: None,
sort_order: now.timestamp_millis(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FavoriteCollection {
pub name: String,
pub description: Option<String>,
entries: HashMap<ClipId, FavoriteEntry>,
order: Vec<ClipId>,
pub created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
}
impl FavoriteCollection {
#[must_use]
pub fn new(name: String) -> Self {
let now = Utc::now();
Self {
name,
description: None,
entries: HashMap::new(),
order: Vec::new(),
created_at: now,
modified_at: now,
}
}
pub fn add(&mut self, clip_id: ClipId) -> bool {
if self.entries.contains_key(&clip_id) {
return false;
}
let entry = FavoriteEntry::new(clip_id);
self.order.push(clip_id);
self.entries.insert(clip_id, entry);
self.modified_at = Utc::now();
true
}
pub fn add_with_note(&mut self, clip_id: ClipId, note: impl Into<String>) -> bool {
let note_str = note.into();
if self.entries.contains_key(&clip_id) {
return false;
}
let mut entry = FavoriteEntry::new(clip_id);
entry.note = Some(note_str);
self.order.push(clip_id);
self.entries.insert(clip_id, entry);
self.modified_at = Utc::now();
true
}
pub fn remove(&mut self, clip_id: &ClipId) -> bool {
if self.entries.remove(clip_id).is_some() {
self.order.retain(|id| id != clip_id);
self.modified_at = Utc::now();
true
} else {
false
}
}
#[must_use]
pub fn contains(&self, clip_id: &ClipId) -> bool {
self.entries.contains_key(clip_id)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn clip_ids(&self) -> Vec<ClipId> {
let mut ordered: Vec<ClipId> = self.order.clone();
ordered.sort_by_key(|id| {
self.entries
.get(id)
.map(|e| e.sort_order)
.unwrap_or(i64::MAX)
});
ordered
}
#[must_use]
pub fn entry(&self, clip_id: &ClipId) -> Option<&FavoriteEntry> {
self.entries.get(clip_id)
}
pub fn set_note(&mut self, clip_id: &ClipId, note: impl Into<String>) -> bool {
if let Some(entry) = self.entries.get_mut(clip_id) {
entry.note = Some(note.into());
self.modified_at = Utc::now();
true
} else {
false
}
}
pub fn move_to(&mut self, clip_id: &ClipId, position: usize) {
let current_pos = match self.order.iter().position(|id| id == clip_id) {
Some(p) => p,
None => return,
};
if position >= self.order.len() {
return;
}
self.order.remove(current_pos);
self.order.insert(position, *clip_id);
for (i, id) in self.order.iter().enumerate() {
if let Some(entry) = self.entries.get_mut(id) {
entry.sort_order = i as i64;
}
}
self.modified_at = Utc::now();
}
pub fn clear(&mut self) {
self.entries.clear();
self.order.clear();
self.modified_at = Utc::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentClipList {
capacity: usize,
list: VecDeque<ClipId>,
access_times: HashMap<ClipId, DateTime<Utc>>,
}
impl RecentClipList {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
list: VecDeque::new(),
access_times: HashMap::new(),
}
}
pub fn record_access(&mut self, clip_id: ClipId) {
if let Some(pos) = self.list.iter().position(|id| *id == clip_id) {
self.list.remove(pos);
} else {
while self.list.len() >= self.capacity {
if let Some(evicted) = self.list.pop_back() {
self.access_times.remove(&evicted);
}
}
}
self.list.push_front(clip_id);
self.access_times.insert(clip_id, Utc::now());
}
#[must_use]
pub fn most_recent(&self) -> Option<ClipId> {
self.list.front().copied()
}
#[must_use]
pub fn clip_ids(&self) -> Vec<ClipId> {
self.list.iter().copied().collect()
}
#[must_use]
pub fn last_access(&self, clip_id: &ClipId) -> Option<DateTime<Utc>> {
self.access_times.get(clip_id).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.list.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.list.is_empty()
}
pub fn remove(&mut self, clip_id: &ClipId) {
if let Some(pos) = self.list.iter().position(|id| id == clip_id) {
self.list.remove(pos);
self.access_times.remove(clip_id);
}
}
pub fn clear(&mut self) {
self.list.clear();
self.access_times.clear();
}
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FavoritesManager {
pub collections: HashMap<String, FavoriteCollection>,
pub recent: RecentClipList,
}
impl FavoritesManager {
#[must_use]
pub fn new(recent_capacity: usize) -> Self {
Self {
collections: HashMap::new(),
recent: RecentClipList::new(recent_capacity),
}
}
pub fn create_collection(&mut self, name: String) -> bool {
if self.collections.contains_key(&name) {
return false;
}
self.collections
.insert(name.clone(), FavoriteCollection::new(name));
true
}
pub fn remove_collection(&mut self, name: &str) -> bool {
self.collections.remove(name).is_some()
}
pub fn add_to_collection(&mut self, collection: &str, clip_id: ClipId) {
self.collections
.entry(collection.to_string())
.or_insert_with(|| FavoriteCollection::new(collection.to_string()))
.add(clip_id);
}
pub fn record_access(&mut self, clip_id: ClipId) {
self.recent.record_access(clip_id);
}
#[must_use]
pub fn collection_names(&self) -> Vec<&str> {
self.collections.keys().map(String::as_str).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn id(n: u8) -> ClipId {
let mut bytes = [0u8; 16];
bytes[15] = n;
ClipId::from_uuid(Uuid::from_bytes(bytes))
}
#[test]
fn test_fav_add_and_contains() {
let mut col = FavoriteCollection::new("Test".to_string());
assert!(col.add(id(1)));
assert!(col.contains(&id(1)));
assert!(!col.contains(&id(2)));
}
#[test]
fn test_fav_no_duplicates() {
let mut col = FavoriteCollection::new("Test".to_string());
assert!(col.add(id(1)));
assert!(!col.add(id(1))); assert_eq!(col.len(), 1);
}
#[test]
fn test_fav_remove() {
let mut col = FavoriteCollection::new("Test".to_string());
col.add(id(1));
assert!(col.remove(&id(1)));
assert!(!col.contains(&id(1)));
assert_eq!(col.len(), 0);
}
#[test]
fn test_fav_set_note() {
let mut col = FavoriteCollection::new("Test".to_string());
col.add(id(1));
assert!(col.set_note(&id(1), "Great shot"));
assert_eq!(
col.entry(&id(1)).and_then(|e| e.note.as_deref()),
Some("Great shot")
);
}
#[test]
fn test_fav_move_to() {
let mut col = FavoriteCollection::new("Test".to_string());
col.add(id(1));
col.add(id(2));
col.add(id(3));
col.move_to(&id(3), 0);
let ids = col.clip_ids();
assert_eq!(ids[0], id(3));
}
#[test]
fn test_fav_clip_ids_sorted() {
let mut col = FavoriteCollection::new("Test".to_string());
col.add(id(1));
col.add(id(2));
col.add(id(3));
let ids = col.clip_ids();
assert_eq!(ids.len(), 3);
}
#[test]
fn test_fav_with_note() {
let mut col = FavoriteCollection::new("T".to_string());
col.add_with_note(id(5), "B-roll pick");
assert_eq!(
col.entry(&id(5)).and_then(|e| e.note.as_deref()),
Some("B-roll pick")
);
}
#[test]
fn test_recent_lru_most_recent() {
let mut r = RecentClipList::new(5);
r.record_access(id(1));
r.record_access(id(2));
assert_eq!(r.most_recent(), Some(id(2)));
}
#[test]
fn test_recent_move_to_front_on_re_access() {
let mut r = RecentClipList::new(5);
r.record_access(id(1));
r.record_access(id(2));
r.record_access(id(1)); assert_eq!(r.most_recent(), Some(id(1)));
assert_eq!(r.len(), 2);
}
#[test]
fn test_recent_eviction_at_capacity() {
let mut r = RecentClipList::new(3);
r.record_access(id(1));
r.record_access(id(2));
r.record_access(id(3));
r.record_access(id(4)); assert_eq!(r.len(), 3);
assert!(!r.clip_ids().contains(&id(1)));
}
#[test]
fn test_recent_remove() {
let mut r = RecentClipList::new(5);
r.record_access(id(1));
r.remove(&id(1));
assert!(r.is_empty());
}
#[test]
fn test_recent_capacity_one() {
let mut r = RecentClipList::new(1);
r.record_access(id(1));
r.record_access(id(2));
assert_eq!(r.len(), 1);
assert_eq!(r.most_recent(), Some(id(2)));
}
#[test]
fn test_manager_create_and_remove_collection() {
let mut mgr = FavoritesManager::new(10);
assert!(mgr.create_collection("picks".to_string()));
assert!(!mgr.create_collection("picks".to_string())); assert!(mgr.remove_collection("picks"));
assert!(!mgr.remove_collection("picks")); }
#[test]
fn test_manager_add_to_and_record() {
let mut mgr = FavoritesManager::new(5);
mgr.add_to_collection("highlights", id(1));
mgr.record_access(id(1));
assert_eq!(mgr.recent.most_recent(), Some(id(1)));
let col = mgr
.collections
.get("highlights")
.expect("collection exists");
assert!(col.contains(&id(1)));
}
}