use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::Result;
pub const VERSION: u32 = 1;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Index {
pub version: u32,
pub root: PathBuf,
pub clips: Vec<Clip>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Span {
pub start: f64,
pub end: f64,
}
impl Span {
pub fn len(self) -> f64 {
self.end - self.start
}
pub fn contains(self, t: f64) -> bool {
self.start <= t && t < self.end
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Clip {
pub id: String,
pub source: PathBuf,
pub analysed: PathBuf,
pub size: u64,
pub mtime: i64,
pub duration: f64,
pub fps: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recorded: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utc: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub take: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strip: Option<String>,
#[serde(default)]
pub strip_times: Vec<f64>,
#[serde(default)]
pub cuts: Vec<f64>,
#[serde(default)]
pub activity: Vec<f32>,
#[serde(default)]
pub luma: Vec<f32>,
#[serde(default)]
pub has_audio: bool,
#[serde(default)]
pub speech: Vec<Span>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transcript: Option<Transcript>,
#[serde(default)]
pub flags: Vec<Flag>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<Location>,
}
impl Clip {
pub fn speech_seconds(&self) -> f64 {
self.speech.iter().map(|s| s.len()).sum()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Transcript {
pub language: String,
pub model: String,
pub segments: Vec<Segment>,
}
impl Transcript {
pub fn words(&self) -> impl Iterator<Item = &Word> {
self.segments.iter().flat_map(|s| &s.words)
}
pub fn text(&self) -> String {
let mut s = String::new();
for seg in &self.segments {
if !s.is_empty() {
s.push(' ');
}
s.push_str(seg.text.trim());
}
s
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Segment {
pub start: f64,
pub end: f64,
pub text: String,
#[serde(default)]
pub words: Vec<Word>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Word {
pub start: f64,
pub end: f64,
pub text: String,
#[serde(default)]
pub prob: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Flag {
pub kind: FlagKind,
pub detail: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<Span>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FlagKind {
Runaway,
Dead,
Dark,
Blip,
NoAudio,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Location {
pub lat: f64,
pub lon: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub source: String,
}
impl Index {
pub fn load(path: &Path) -> Result<Self> {
let s = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&s)?)
}
pub fn save(&self, path: &Path) -> Result<()> {
write_json(path, self)
}
pub fn clip(&self, id: &str) -> Option<&Clip> {
self.clips.iter().find(|c| c.id == id)
}
}
pub(crate) fn write_json<T: Serialize>(path: &Path, v: &T) -> Result<()> {
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, serde_json::to_vec(v)?)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
pub fn timecode(t: f64) -> String {
let t = t.max(0.0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let whole = t as u64;
let frac = t - whole as f64;
let (h, m, s) = (whole / 3600, whole / 60 % 60, whole % 60);
let mut out = String::new();
if h > 0 {
let _ = write!(out, "{h}:{m:02}:");
} else {
let _ = write!(out, "{m}:");
}
let _ = write!(out, "{:05.2}", s as f64 + frac);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timecodes() {
assert_eq!(timecode(0.0), "0:00.00");
assert_eq!(timecode(61.5), "1:01.50");
assert_eq!(timecode(16_200.25), "4:30:00.25");
}
#[test]
fn unknown_fields_are_ignored() {
let j = r#"{"version":1,"root":"/x","clips":[{"id":"a","source":"a.mp4","analysed":"a.lrf",
"size":1,"mtime":2,"duration":3.0,"fps":24.0,"future":true}]}"#;
let ix: Index = serde_json::from_str(j).expect("parse");
assert_eq!(ix.clips[0].id, "a");
assert_eq!(ix.clips[0].speech, []);
}
}