#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProxyResolution {
Quarter,
Half,
Full,
Custom(u32, u32),
}
impl ProxyResolution {
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn megapixels(&self) -> f32 {
const SRC_W: u32 = 3840;
const SRC_H: u32 = 2160;
match self {
Self::Quarter => {
let w = SRC_W / 4;
let h = SRC_H / 4;
(w * h) as f32 / 1_000_000.0
}
Self::Half => {
let w = SRC_W / 2;
let h = SRC_H / 2;
(w * h) as f32 / 1_000_000.0
}
Self::Full => (SRC_W * SRC_H) as f32 / 1_000_000.0,
Self::Custom(w, h) => (*w * *h) as f32 / 1_000_000.0,
}
}
#[must_use]
pub fn label(&self) -> String {
match self {
Self::Quarter => "1/4".to_string(),
Self::Half => "1/2".to_string(),
Self::Full => "Full".to_string(),
Self::Custom(w, h) => format!("{w}x{h}"),
}
}
}
#[derive(Debug, Clone)]
pub struct ProxyLink {
pub clip_id: u64,
pub proxy_path: PathBuf,
pub resolution: ProxyResolution,
pub verified: bool,
}
impl ProxyLink {
#[must_use]
pub fn new(clip_id: u64, proxy_path: PathBuf, resolution: ProxyResolution) -> Self {
Self {
clip_id,
proxy_path,
resolution,
verified: false,
}
}
pub fn verify(&mut self) {
self.verified = true;
}
#[must_use]
pub fn has_proxy(&self) -> bool {
self.verified
}
#[must_use]
pub fn is_offline_edit_suitable(&self) -> bool {
matches!(
self.resolution,
ProxyResolution::Quarter | ProxyResolution::Half
)
}
}
#[derive(Debug, Default)]
pub struct ProxyLinkRegistry {
links: HashMap<u64, Vec<ProxyLink>>,
}
impl ProxyLinkRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn link(&mut self, proxy: ProxyLink) {
let entry = self.links.entry(proxy.clip_id).or_default();
if let Some(existing) = entry.iter_mut().find(|l| l.resolution == proxy.resolution) {
*existing = proxy;
} else {
entry.push(proxy);
}
}
pub fn unlink(&mut self, clip_id: u64, resolution: &ProxyResolution) -> bool {
let Some(entry) = self.links.get_mut(&clip_id) else {
return false;
};
let before = entry.len();
entry.retain(|l| &l.resolution != resolution);
entry.len() < before
}
#[must_use]
pub fn find_proxy(&self, clip_id: u64, resolution: &ProxyResolution) -> Option<&ProxyLink> {
self.links
.get(&clip_id)?
.iter()
.find(|l| &l.resolution == resolution)
}
#[must_use]
pub fn all_proxies(&self, clip_id: u64) -> &[ProxyLink] {
self.links.get(&clip_id).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn total_link_count(&self) -> usize {
self.links.values().map(Vec::len).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path(s: &str) -> PathBuf {
PathBuf::from(s)
}
fn tmp_path(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("oximedia-clips-proxy-link-{name}"))
}
#[test]
fn test_megapixels_quarter() {
let mp = ProxyResolution::Quarter.megapixels();
assert!((mp - 0.5184_f32).abs() < 1e-3);
}
#[test]
fn test_megapixels_half() {
let mp = ProxyResolution::Half.megapixels();
assert!((mp - 2.0736_f32).abs() < 1e-3);
}
#[test]
fn test_megapixels_full() {
let mp = ProxyResolution::Full.megapixels();
assert!((mp - 8.2944_f32).abs() < 1e-3);
}
#[test]
fn test_megapixels_custom() {
let mp = ProxyResolution::Custom(1280, 720).megapixels();
assert!((mp - 0.9216_f32).abs() < 1e-3);
}
#[test]
fn test_proxy_resolution_label() {
assert_eq!(ProxyResolution::Quarter.label(), "1/4");
assert_eq!(ProxyResolution::Custom(1920, 1080).label(), "1920x1080");
}
#[test]
fn test_proxy_link_not_verified_initially() {
let link = ProxyLink::new(1, tmp_path("proxy.mov"), ProxyResolution::Half);
assert!(!link.has_proxy());
}
#[test]
fn test_proxy_link_verified_after_verify() {
let mut link = ProxyLink::new(1, tmp_path("proxy.mov"), ProxyResolution::Half);
link.verify();
assert!(link.has_proxy());
}
#[test]
fn test_proxy_link_offline_edit_suitable_quarter() {
let link = ProxyLink::new(1, tmp_path("q.mov"), ProxyResolution::Quarter);
assert!(link.is_offline_edit_suitable());
}
#[test]
fn test_proxy_link_full_not_offline_edit_suitable() {
let link = ProxyLink::new(1, tmp_path("f.mov"), ProxyResolution::Full);
assert!(!link.is_offline_edit_suitable());
}
#[test]
fn test_registry_link_and_find() {
let mut reg = ProxyLinkRegistry::new();
let proxy = ProxyLink::new(42, tmp_path("42_half.mov"), ProxyResolution::Half);
reg.link(proxy);
assert!(reg.find_proxy(42, &ProxyResolution::Half).is_some());
}
#[test]
fn test_registry_find_missing() {
let reg = ProxyLinkRegistry::new();
assert!(reg.find_proxy(99, &ProxyResolution::Quarter).is_none());
}
#[test]
fn test_registry_link_replaces_same_resolution() {
let mut reg = ProxyLinkRegistry::new();
reg.link(ProxyLink::new(1, path("/old.mov"), ProxyResolution::Half));
reg.link(ProxyLink::new(1, path("/new.mov"), ProxyResolution::Half));
let found = reg
.find_proxy(1, &ProxyResolution::Half)
.expect("find_proxy should succeed");
assert_eq!(found.proxy_path, path("/new.mov"));
}
#[test]
fn test_registry_unlink() {
let mut reg = ProxyLinkRegistry::new();
reg.link(ProxyLink::new(5, path("/p.mov"), ProxyResolution::Quarter));
assert!(reg.unlink(5, &ProxyResolution::Quarter));
assert!(reg.find_proxy(5, &ProxyResolution::Quarter).is_none());
}
#[test]
fn test_registry_unlink_missing_returns_false() {
let mut reg = ProxyLinkRegistry::new();
assert!(!reg.unlink(999, &ProxyResolution::Half));
}
#[test]
fn test_registry_total_link_count() {
let mut reg = ProxyLinkRegistry::new();
reg.link(ProxyLink::new(1, path("/a.mov"), ProxyResolution::Quarter));
reg.link(ProxyLink::new(1, path("/b.mov"), ProxyResolution::Half));
reg.link(ProxyLink::new(2, path("/c.mov"), ProxyResolution::Full));
assert_eq!(reg.total_link_count(), 3);
}
}