use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct CachedSourceMetadata {
pub path: String,
pub width: u32,
pub height: u32,
pub duration_secs: f64,
pub video_bitrate: u64,
pub audio_bitrate: u64,
pub frame_rate: f64,
pub codec: String,
pub keyframe_positions_secs: Vec<f64>,
pub container: String,
}
impl CachedSourceMetadata {
#[must_use]
pub fn minimal(path: impl Into<String>) -> Self {
Self {
path: path.into(),
width: 0,
height: 0,
duration_secs: 0.0,
video_bitrate: 0,
audio_bitrate: 0,
frame_rate: 0.0,
codec: String::new(),
keyframe_positions_secs: Vec::new(),
container: String::new(),
}
}
#[must_use]
pub fn total_bitrate(&self) -> u64 {
self.video_bitrate + self.audio_bitrate
}
#[must_use]
pub fn has_keyframes(&self) -> bool {
!self.keyframe_positions_secs.is_empty()
}
#[must_use]
pub fn keyframe_before(&self, position_secs: f64) -> Option<f64> {
self.keyframe_positions_secs
.iter()
.filter(|&&k| k <= position_secs)
.copied()
.next_back()
}
#[must_use]
pub fn keyframe_after(&self, position_secs: f64) -> Option<f64> {
self.keyframe_positions_secs
.iter()
.find(|&&k| k >= position_secs)
.copied()
}
}
struct CacheEntry {
metadata: CachedSourceMetadata,
inserted_at: Instant,
hit_count: u64,
}
pub struct MetadataCache {
entries: HashMap<String, CacheEntry>,
capacity: Option<usize>,
ttl: Option<Duration>,
}
impl Default for MetadataCache {
fn default() -> Self {
Self::new()
}
}
impl MetadataCache {
#[must_use]
pub fn new() -> Self {
Self {
entries: HashMap::new(),
capacity: None,
ttl: None,
}
}
#[must_use]
pub fn with_capacity(capacity: usize, ttl: Option<Duration>) -> Self {
Self {
entries: HashMap::with_capacity(capacity),
capacity: Some(capacity),
ttl,
}
}
pub fn insert(&mut self, meta: CachedSourceMetadata) {
let key = meta.path.clone();
if let Some(cap) = self.capacity {
if self.entries.len() >= cap && !self.entries.contains_key(&key) {
self.evict_oldest();
}
}
self.entries.insert(
key,
CacheEntry {
metadata: meta,
inserted_at: Instant::now(),
hit_count: 0,
},
);
}
pub fn get(&mut self, path: &str) -> Option<&CachedSourceMetadata> {
if let Some(ttl) = self.ttl {
if let Some(entry) = self.entries.get(path) {
if entry.inserted_at.elapsed() > ttl {
self.entries.remove(path);
return None;
}
}
}
let entry = self.entries.get_mut(path)?;
entry.hit_count += 1;
Some(&entry.metadata)
}
#[must_use]
pub fn peek(&self, path: &str) -> Option<&CachedSourceMetadata> {
self.entries.get(path).map(|e| &e.metadata)
}
pub fn evict(&mut self, path: &str) -> bool {
self.entries.remove(path).is_some()
}
pub fn evict_expired(&mut self) -> usize {
let ttl = match self.ttl {
Some(t) => t,
None => return 0,
};
let before = self.entries.len();
self.entries.retain(|_, e| e.inserted_at.elapsed() <= ttl);
before - self.entries.len()
}
pub fn clear(&mut self) {
self.entries.clear();
}
#[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 hit_count(&self, path: &str) -> u64 {
self.entries.get(path).map_or(0, |e| e.hit_count)
}
fn evict_oldest(&mut self) {
let oldest_key = self
.entries
.iter()
.min_by_key(|(_, e)| e.inserted_at)
.map(|(k, _)| k.clone());
if let Some(key) = oldest_key {
self.entries.remove(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_meta(path: &str) -> CachedSourceMetadata {
CachedSourceMetadata {
path: path.to_string(),
width: 1920,
height: 1080,
duration_secs: 60.0,
video_bitrate: 5_000_000,
audio_bitrate: 128_000,
frame_rate: 25.0,
codec: "av1".to_string(),
keyframe_positions_secs: vec![0.0, 2.0, 4.0, 6.0],
container: "mkv".to_string(),
}
}
#[test]
fn test_insert_and_get() {
let mut cache = MetadataCache::new();
cache.insert(make_meta("/a.mkv"));
assert!(cache.get("/a.mkv").is_some());
assert!(cache.get("/b.mkv").is_none());
}
#[test]
fn test_evict() {
let mut cache = MetadataCache::new();
cache.insert(make_meta("/a.mkv"));
assert!(cache.evict("/a.mkv"));
assert!(!cache.evict("/a.mkv")); assert!(cache.get("/a.mkv").is_none());
}
#[test]
fn test_capacity_evicts_oldest() {
let mut cache = MetadataCache::with_capacity(2, None);
cache.insert(make_meta("/a.mkv"));
cache.insert(make_meta("/b.mkv"));
cache.insert(make_meta("/c.mkv"));
assert_eq!(cache.len(), 2);
assert!(cache.len() <= 2);
}
#[test]
fn test_ttl_expiry() {
let mut cache = MetadataCache::with_capacity(10, Some(Duration::from_millis(1)));
cache.insert(make_meta("/a.mkv"));
std::thread::sleep(Duration::from_millis(5));
assert!(cache.get("/a.mkv").is_none());
}
#[test]
fn test_hit_count() {
let mut cache = MetadataCache::new();
cache.insert(make_meta("/a.mkv"));
let _ = cache.get("/a.mkv");
let _ = cache.get("/a.mkv");
let _ = cache.get("/a.mkv");
assert_eq!(cache.hit_count("/a.mkv"), 3);
}
#[test]
fn test_evict_expired_no_ttl() {
let mut cache = MetadataCache::new();
cache.insert(make_meta("/a.mkv"));
let evicted = cache.evict_expired();
assert_eq!(evicted, 0);
}
#[test]
fn test_clear() {
let mut cache = MetadataCache::new();
cache.insert(make_meta("/a.mkv"));
cache.insert(make_meta("/b.mkv"));
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_metadata_total_bitrate() {
let meta = make_meta("/a.mkv");
assert_eq!(meta.total_bitrate(), 5_128_000);
}
#[test]
fn test_keyframe_before() {
let meta = make_meta("/a.mkv");
assert_eq!(meta.keyframe_before(3.5), Some(2.0));
assert_eq!(meta.keyframe_before(0.0), Some(0.0));
assert_eq!(meta.keyframe_before(-1.0), None);
}
#[test]
fn test_keyframe_after() {
let meta = make_meta("/a.mkv");
assert_eq!(meta.keyframe_after(3.5), Some(4.0));
assert_eq!(meta.keyframe_after(6.0), Some(6.0));
assert_eq!(meta.keyframe_after(7.0), None);
}
#[test]
fn test_peek_does_not_update_hit_count() {
let mut cache = MetadataCache::new();
cache.insert(make_meta("/a.mkv"));
let _ = cache.peek("/a.mkv");
assert_eq!(cache.hit_count("/a.mkv"), 0);
}
#[test]
fn test_minimal_constructor() {
let m = CachedSourceMetadata::minimal("/f.mkv");
assert_eq!(m.path, "/f.mkv");
assert_eq!(m.total_bitrate(), 0);
assert!(!m.has_keyframes());
}
}