#![forbid(unsafe_code)]
use super::{matroska::MatroskaChapter, mp4::Mp4Chapter};
#[derive(Debug, Clone)]
pub struct ChapterGeneratorConfig {
pub interval_secs: f64,
pub title_prefix: String,
pub number_chapters: bool,
pub language: String,
}
impl Default for ChapterGeneratorConfig {
fn default() -> Self {
Self {
interval_secs: 300.0, title_prefix: "Chapter".into(),
number_chapters: true,
language: "eng".into(),
}
}
}
impl ChapterGeneratorConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_interval(mut self, interval_secs: f64) -> Self {
self.interval_secs = interval_secs;
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.title_prefix = prefix.into();
self
}
}
pub struct ChapterGenerator {
config: ChapterGeneratorConfig,
}
impl ChapterGenerator {
#[must_use]
pub fn new(config: ChapterGeneratorConfig) -> Self {
Self { config }
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn generate_interval_chapters(&self, duration_secs: f64) -> Vec<MatroskaChapter> {
let mut chapters = Vec::new();
let mut time = 0.0;
let mut chapter_num = 1;
while time < duration_secs {
let title = if self.config.number_chapters {
format!("{} {}", self.config.title_prefix, chapter_num)
} else {
self.config.title_prefix.clone()
};
let chapter = MatroskaChapter::new(chapter_num, (time * 1_000_000_000.0) as u64)
.with_display(&self.config.language, title);
chapters.push(chapter);
time += self.config.interval_secs;
chapter_num += 1;
}
chapters
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn generate_from_timestamps(
&self,
timestamps_secs: &[f64],
titles: Option<&[String]>,
) -> Vec<MatroskaChapter> {
timestamps_secs
.iter()
.enumerate()
.map(|(i, &time)| {
let title = titles
.and_then(|t| t.get(i))
.cloned()
.unwrap_or_else(|| format!("{} {}", self.config.title_prefix, i + 1));
MatroskaChapter::new((i + 1) as u64, (time * 1_000_000_000.0) as u64)
.with_display(&self.config.language, title)
})
.collect()
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn generate_mp4_chapters(&self, duration_secs: f64) -> Vec<Mp4Chapter> {
let mut chapters = Vec::new();
let mut time = 0.0;
let mut chapter_num = 1;
while time < duration_secs {
let title = if self.config.number_chapters {
format!("{} {}", self.config.title_prefix, chapter_num)
} else {
self.config.title_prefix.clone()
};
chapters.push(Mp4Chapter::new((time * 1000.0) as u64, title));
time += self.config.interval_secs;
chapter_num += 1;
}
chapters
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chapter_generator_config() {
let config = ChapterGeneratorConfig::new()
.with_interval(60.0)
.with_prefix("Part");
assert_eq!(config.interval_secs, 60.0);
assert_eq!(config.title_prefix, "Part");
}
#[test]
fn test_generate_interval_chapters() {
let config = ChapterGeneratorConfig::new().with_interval(10.0);
let generator = ChapterGenerator::new(config);
let chapters = generator.generate_interval_chapters(30.0);
assert_eq!(chapters.len(), 3);
assert_eq!(chapters[0].start_time_ns, 0);
assert_eq!(chapters[1].start_time_ns, 10_000_000_000);
}
#[test]
fn test_generate_from_timestamps() {
let config = ChapterGeneratorConfig::new();
let generator = ChapterGenerator::new(config);
let timestamps = vec![0.0, 5.0, 10.0];
let titles = vec!["Intro".to_string(), "Middle".to_string(), "End".to_string()];
let chapters = generator.generate_from_timestamps(×tamps, Some(&titles));
assert_eq!(chapters.len(), 3);
assert_eq!(chapters[0].default_title(), Some("Intro"));
}
#[test]
fn test_generate_mp4_chapters() {
let config = ChapterGeneratorConfig::new().with_interval(5.0);
let generator = ChapterGenerator::new(config);
let chapters = generator.generate_mp4_chapters(15.0);
assert_eq!(chapters.len(), 3);
assert_eq!(chapters[0].start_time_ms, 0);
assert_eq!(chapters[1].start_time_ms, 5000);
}
}