#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CueType {
Chapter,
AdBreak,
Bookmark,
SceneChange,
ForcedSubtitle,
Custom(String),
}
impl CueType {
#[must_use]
pub fn label(&self) -> &str {
match self {
Self::Chapter => "Chapter",
Self::AdBreak => "AdBreak",
Self::Bookmark => "Bookmark",
Self::SceneChange => "SceneChange",
Self::ForcedSubtitle => "ForcedSubtitle",
Self::Custom(s) => s.as_str(),
}
}
}
#[derive(Debug, Clone)]
pub struct CuePoint {
pub cue_type: CueType,
pub timestamp_ms: i64,
pub label: Option<String>,
pub duration_ms: Option<i64>,
}
impl CuePoint {
#[must_use]
pub fn new(cue_type: CueType, timestamp_ms: i64) -> Self {
Self {
cue_type,
timestamp_ms,
label: None,
duration_ms: None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_duration(mut self, duration_ms: i64) -> Self {
self.duration_ms = Some(duration_ms);
self
}
#[must_use]
pub fn in_range(&self, from_ms: i64, to_ms: i64) -> bool {
self.timestamp_ms >= from_ms && self.timestamp_ms < to_ms
}
}
#[derive(Debug, Default)]
pub struct CuePointList {
points: Vec<CuePoint>,
}
impl CuePointList {
#[must_use]
pub fn new() -> Self {
Self { points: Vec::new() }
}
pub fn add(&mut self, cue: CuePoint) {
let pos = self
.points
.partition_point(|p| p.timestamp_ms <= cue.timestamp_ms);
self.points.insert(pos, cue);
}
#[must_use]
pub fn len(&self) -> usize {
self.points.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
#[must_use]
pub fn in_range(&self, from_ms: i64, to_ms: i64) -> Vec<&CuePoint> {
self.points
.iter()
.filter(|p| p.in_range(from_ms, to_ms))
.collect()
}
#[must_use]
pub fn by_type(&self, cue_type: &CueType) -> Vec<&CuePoint> {
self.points
.iter()
.filter(|p| &p.cue_type == cue_type)
.collect()
}
#[must_use]
pub fn latest_at(&self, timestamp_ms: i64) -> Option<&CuePoint> {
self.points
.iter()
.rev()
.find(|p| p.timestamp_ms <= timestamp_ms)
}
pub fn remove_type(&mut self, cue_type: &CueType) {
self.points.retain(|p| &p.cue_type != cue_type);
}
pub fn iter(&self) -> impl Iterator<Item = &CuePoint> {
self.points.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_list() -> CuePointList {
let mut list = CuePointList::new();
list.add(CuePoint::new(CueType::Chapter, 0).with_label("Intro"));
list.add(CuePoint::new(CueType::AdBreak, 30_000).with_duration(15_000));
list.add(CuePoint::new(CueType::Chapter, 60_000).with_label("Act 1"));
list.add(CuePoint::new(CueType::SceneChange, 45_000));
list.add(CuePoint::new(CueType::Bookmark, 90_000).with_label("Fav"));
list
}
#[test]
fn test_list_len() {
let list = make_list();
assert_eq!(list.len(), 5);
}
#[test]
fn test_list_is_sorted() {
let list = make_list();
let ts: Vec<i64> = list.iter().map(|p| p.timestamp_ms).collect();
let mut sorted = ts.clone();
sorted.sort_unstable();
assert_eq!(ts, sorted);
}
#[test]
fn test_in_range_basic() {
let list = make_list();
let hits = list.in_range(0, 31_000);
assert_eq!(hits.len(), 2);
}
#[test]
fn test_in_range_exclusive_end() {
let list = make_list();
let hits = list.in_range(0, 30_000);
assert!(!hits.iter().any(|p| p.timestamp_ms == 30_000));
}
#[test]
fn test_in_range_empty() {
let list = make_list();
let hits = list.in_range(200_000, 300_000);
assert!(hits.is_empty());
}
#[test]
fn test_by_type_chapter() {
let list = make_list();
let chapters = list.by_type(&CueType::Chapter);
assert_eq!(chapters.len(), 2);
}
#[test]
fn test_by_type_no_match() {
let list = make_list();
let custom = list.by_type(&CueType::Custom("foo".to_string()));
assert!(custom.is_empty());
}
#[test]
fn test_latest_at() {
let list = make_list();
let cp = list.latest_at(50_000).expect("should succeed in test");
assert_eq!(cp.timestamp_ms, 45_000);
}
#[test]
fn test_latest_at_none() {
let list = make_list();
assert!(list.latest_at(-1).is_none());
}
#[test]
fn test_remove_type() {
let mut list = make_list();
list.remove_type(&CueType::Chapter);
assert!(list.by_type(&CueType::Chapter).is_empty());
assert_eq!(list.len(), 3);
}
#[test]
fn test_cue_point_in_range() {
let cp = CuePoint::new(CueType::Bookmark, 5000);
assert!(cp.in_range(4000, 6000));
assert!(cp.in_range(5000, 6000));
assert!(!cp.in_range(5001, 6000));
assert!(!cp.in_range(6000, 9000));
}
#[test]
fn test_cue_type_label_custom() {
let t = CueType::Custom("my-event".to_string());
assert_eq!(t.label(), "my-event");
}
#[test]
fn test_cue_type_label_standard() {
assert_eq!(CueType::Chapter.label(), "Chapter");
assert_eq!(CueType::AdBreak.label(), "AdBreak");
assert_eq!(CueType::SceneChange.label(), "SceneChange");
}
#[test]
fn test_with_duration() {
let cp = CuePoint::new(CueType::AdBreak, 0).with_duration(30_000);
assert_eq!(cp.duration_ms, Some(30_000));
}
#[test]
fn test_empty_list() {
let list = CuePointList::new();
assert!(list.is_empty());
assert!(list.latest_at(9999).is_none());
assert!(list.in_range(0, 1000).is_empty());
}
}