use derive_more::{Display, IsVariant};
#[cfg(any(feature = "std", feature = "alloc"))]
use crate::domain::{
bitflags::{AudioIndexStatus, SubtitleIndexStatus, VideoIndexStatus},
primitives::{ErrorCode, ErrorInfo},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum MediaKind {
#[default]
Video,
Audio,
}
impl MediaKind {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Video => "video",
Self::Audio => "audio",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"video" => Self::Video,
"audio" => Self::Audio,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Display)]
#[display("{}", self.as_str())]
#[non_exhaustive]
pub enum SceneDetector {
Histogram,
Phash,
Threshold,
Content,
Adaptive,
Manual,
}
impl SceneDetector {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Histogram => "histogram",
Self::Phash => "phash",
Self::Threshold => "threshold",
Self::Content => "content",
Self::Adaptive => "adaptive",
Self::Manual => "manual",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"histogram" => Self::Histogram,
"phash" => Self::Phash,
"threshold" => Self::Threshold,
"content" => Self::Content,
"adaptive" => Self::Adaptive,
"manual" => Self::Manual,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Display)]
#[display("{}", self.as_str())]
#[non_exhaustive]
pub enum KeyframeExtractor {
Histogram,
Phash,
Threshold,
Content,
Adaptive,
CompositeQuality,
Interval,
IFrame,
SceneRepresentative,
Manual,
}
impl KeyframeExtractor {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Histogram => "histogram",
Self::Phash => "phash",
Self::Threshold => "threshold",
Self::Content => "content",
Self::Adaptive => "adaptive",
Self::CompositeQuality => "composite_quality",
Self::Interval => "interval",
Self::IFrame => "i_frame",
Self::SceneRepresentative => "scene_representative",
Self::Manual => "manual",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"histogram" => Self::Histogram,
"phash" => Self::Phash,
"threshold" => Self::Threshold,
"content" => Self::Content,
"adaptive" => Self::Adaptive,
"composite_quality" => Self::CompositeQuality,
"interval" => Self::Interval,
"i_frame" => Self::IFrame,
"scene_representative" => Self::SceneRepresentative,
"manual" => Self::Manual,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum SubtitleKind {
#[default]
FullDialogue,
ForcedNarrative,
CommentaryText,
}
impl SubtitleKind {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::FullDialogue => "full_dialogue",
Self::ForcedNarrative => "forced_narrative",
Self::CommentaryText => "commentary_text",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"full_dialogue" => Self::FullDialogue,
"forced_narrative" => Self::ForcedNarrative,
"commentary_text" => Self::CommentaryText,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum AudioContentKind {
#[default]
Speech,
Music,
Mixed,
Silence,
}
impl AudioContentKind {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Speech => "speech",
Self::Music => "music",
Self::Mixed => "mixed",
Self::Silence => "silence",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"speech" => Self::Speech,
"music" => Self::Music,
"mixed" => Self::Mixed,
"silence" => Self::Silence,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum ScanStatus {
#[default]
Ok,
Partial,
Failed,
}
impl ScanStatus {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Ok => "ok",
Self::Partial => "partial",
Self::Failed => "failed",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"ok" => Self::Ok,
"partial" => Self::Partial,
"failed" => Self::Failed,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum VideoIndexStage {
#[default]
Pending,
Probed,
SceneDetected,
KeyframeExtracted,
Analyzed,
Embedded,
Done,
Failed,
}
impl VideoIndexStage {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Probed => "probed",
Self::SceneDetected => "scene_detected",
Self::KeyframeExtracted => "keyframe_extracted",
Self::Analyzed => "analyzed",
Self::Embedded => "embedded",
Self::Done => "done",
Self::Failed => "failed",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"pending" => Self::Pending,
"probed" => Self::Probed,
"scene_detected" => Self::SceneDetected,
"keyframe_extracted" => Self::KeyframeExtracted,
"analyzed" => Self::Analyzed,
"embedded" => Self::Embedded,
"done" => Self::Done,
"failed" => Self::Failed,
_ => return None,
})
}
}
#[cfg(any(feature = "std", feature = "alloc"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "alloc"))))]
impl VideoIndexStage {
fn error_stage_bit(code: ErrorCode) -> Option<VideoIndexStatus> {
use VideoIndexStatus as S;
match code {
ErrorCode::ProbeCorrupt
| ErrorCode::ProbeUnsupportedFormat
| ErrorCode::ProbeNoVideoStream
| ErrorCode::ProbeNoAudioStream => Some(S::PROBED),
ErrorCode::SceneDetectionFailed | ErrorCode::SceneDetectionModelError => {
Some(S::SCENE_DETECTED)
}
ErrorCode::VlmFailed | ErrorCode::VlmModelError => Some(S::VLM_ANALYZED),
ErrorCode::AppleVisionFailed | ErrorCode::AppleVisionRequestFailed => {
Some(S::APPLE_VISION_ANALYZED)
}
ErrorCode::EmbeddingFailed
| ErrorCode::EmbeddingModelError
| ErrorCode::EmbeddingModelLoadFailed
| ErrorCode::EmbeddingPreprocessFailed
| ErrorCode::EmbeddingInferenceFailed
| ErrorCode::EmbeddingOutputInvalid => {
Some(S::TEXT_EMBEDDING_FINISHED | S::SCENE_EMBEDDING_FINISHED)
}
_ => None,
}
}
fn is_live(status: VideoIndexStatus, e: &ErrorInfo) -> bool {
match Self::error_stage_bit(e.code()) {
Some(bit) => !status.intersects(bit),
None => true,
}
}
pub fn from_status(status: VideoIndexStatus, errors: &[ErrorInfo]) -> Self {
use VideoIndexStatus as S;
if errors.iter().any(|e| Self::is_live(status, e)) {
return Self::Failed;
}
if status.is_fully_indexed() {
return Self::Done;
}
if !status.contains(S::PROBED) {
return Self::Pending;
}
if !status.contains(S::SCENE_DETECTED) {
return Self::Probed;
}
if !status.contains(S::KEYFRAME_EXTRACTED) {
return Self::SceneDetected;
}
if !status.intersects(S::VLM_ANALYZED | S::APPLE_VISION_ANALYZED) {
return Self::KeyframeExtracted;
}
if !status.intersects(S::TEXT_EMBEDDING_FINISHED | S::SCENE_EMBEDDING_FINISHED) {
return Self::Analyzed;
}
Self::Embedded
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum AudioIndexStage {
#[default]
Pending,
Extracted,
Analyzed,
Transcribed,
Diarized,
Embedded,
Done,
Failed,
}
impl AudioIndexStage {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Extracted => "extracted",
Self::Analyzed => "analyzed",
Self::Transcribed => "transcribed",
Self::Diarized => "diarized",
Self::Embedded => "embedded",
Self::Done => "done",
Self::Failed => "failed",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"pending" => Self::Pending,
"extracted" => Self::Extracted,
"analyzed" => Self::Analyzed,
"transcribed" => Self::Transcribed,
"diarized" => Self::Diarized,
"embedded" => Self::Embedded,
"done" => Self::Done,
"failed" => Self::Failed,
_ => return None,
})
}
}
#[cfg(any(feature = "std", feature = "alloc"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "alloc"))))]
impl AudioIndexStage {
fn error_stage_bit(code: ErrorCode) -> Option<AudioIndexStatus> {
use AudioIndexStatus as S;
match code {
ErrorCode::ProbeCorrupt
| ErrorCode::ProbeUnsupportedFormat
| ErrorCode::ProbeNoAudioStream
| ErrorCode::ProbeNoVideoStream => Some(S::EXTRACTED),
ErrorCode::CedFailed | ErrorCode::CedRequestFailed | ErrorCode::CedModelError => {
Some(S::CED_DONE)
}
ErrorCode::TranscriptionFailed | ErrorCode::TranscriptionModelError => Some(S::STT_DONE),
ErrorCode::EmbeddingFailed
| ErrorCode::EmbeddingModelError
| ErrorCode::EmbeddingModelLoadFailed
| ErrorCode::EmbeddingPreprocessFailed
| ErrorCode::EmbeddingInferenceFailed
| ErrorCode::EmbeddingOutputInvalid => Some(S::TEXT_EMBED),
_ => None,
}
}
fn is_live(status: AudioIndexStatus, e: &ErrorInfo) -> bool {
match Self::error_stage_bit(e.code()) {
Some(bit) => !status.intersects(bit),
None => true,
}
}
pub fn from_status(status: AudioIndexStatus, errors: &[ErrorInfo]) -> Self {
use AudioIndexStatus as S;
if errors.iter().any(|e| Self::is_live(status, e)) {
return Self::Failed;
}
if status.is_fully_indexed() {
return Self::Done;
}
if !status.contains(S::EXTRACTED) {
return Self::Pending;
}
if !status.intersects(S::CLASSIFIED | S::VAD_DONE) {
return Self::Extracted;
}
if !status.contains(S::STT_DONE) {
return Self::Analyzed;
}
if !status.contains(S::SPEAKER_DONE) {
return Self::Transcribed;
}
if !status.contains(S::TEXT_EMBED) {
return Self::Diarized;
}
Self::Embedded
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum SubtitleIndexStage {
#[default]
Pending,
TracksDiscovered,
CuesExtracted,
Ocr,
SearchIndexed,
Done,
Failed,
}
impl SubtitleIndexStage {
#[inline(always)]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::TracksDiscovered => "tracks_discovered",
Self::CuesExtracted => "cues_extracted",
Self::Ocr => "ocr",
Self::SearchIndexed => "search_indexed",
Self::Done => "done",
Self::Failed => "failed",
}
}
#[inline]
pub fn from_str(s: &str) -> Option<Self> {
Some(match s {
"pending" => Self::Pending,
"tracks_discovered" => Self::TracksDiscovered,
"cues_extracted" => Self::CuesExtracted,
"ocr" => Self::Ocr,
"search_indexed" => Self::SearchIndexed,
"done" => Self::Done,
"failed" => Self::Failed,
_ => return None,
})
}
}
#[cfg(any(feature = "std", feature = "alloc"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "alloc"))))]
impl SubtitleIndexStage {
#[allow(dead_code)]
fn error_stage_bit(code: ErrorCode, requires_ocr: bool) -> Option<SubtitleIndexStatus> {
use SubtitleIndexStatus as S;
match code {
ErrorCode::ProbeCorrupt
| ErrorCode::ProbeUnsupportedFormat
| ErrorCode::ProbeNoVideoStream => Some(S::TRACKS_DISCOVERED),
ErrorCode::AppleVisionFailed | ErrorCode::AppleVisionRequestFailed if requires_ocr => {
Some(S::OCR_DONE)
}
_ => None,
}
}
#[allow(dead_code)]
fn is_live(status: SubtitleIndexStatus, requires_ocr: bool, e: &ErrorInfo) -> bool {
match Self::error_stage_bit(e.code(), requires_ocr) {
Some(bit) => !status.intersects(bit),
None => true,
}
}
#[allow(dead_code)]
pub(crate) fn from_status(
status: SubtitleIndexStatus,
requires_ocr: bool,
errors: &[ErrorInfo],
) -> Self {
use SubtitleIndexStatus as S;
if errors
.iter()
.any(|e| Self::is_live(status, requires_ocr, e))
{
return Self::Failed;
}
let mask = SubtitleIndexStatus::fully_indexed_mask(requires_ocr);
if status.contains(mask) {
return Self::Done;
}
if !status.contains(S::TRACKS_DISCOVERED) {
return Self::Pending;
}
if !status.contains(S::CUES_EXTRACTED) {
return Self::TracksDiscovered;
}
if requires_ocr && !status.contains(S::OCR_DONE) {
return Self::CuesExtracted;
}
if !status.contains(S::SEARCH_INDEXED) {
return if requires_ocr {
Self::Ocr
} else {
Self::CuesExtracted
};
}
Self::SearchIndexed
}
}
#[cfg(test)]
mod slug_tests {
use super::*;
#[test]
fn media_kind_slug_roundtrip() {
for v in [MediaKind::Video, MediaKind::Audio] {
assert_eq!(MediaKind::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(MediaKind::from_str("not_a_slug"), None);
}
#[test]
fn scene_detector_slug_roundtrip() {
for v in [
SceneDetector::Histogram,
SceneDetector::Phash,
SceneDetector::Threshold,
SceneDetector::Content,
SceneDetector::Adaptive,
SceneDetector::Manual,
] {
assert_eq!(SceneDetector::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(SceneDetector::from_str("not_a_slug"), None);
}
#[test]
fn keyframe_extractor_slug_roundtrip() {
for v in [
KeyframeExtractor::Histogram,
KeyframeExtractor::Phash,
KeyframeExtractor::Threshold,
KeyframeExtractor::Content,
KeyframeExtractor::Adaptive,
KeyframeExtractor::CompositeQuality,
KeyframeExtractor::Interval,
KeyframeExtractor::IFrame,
KeyframeExtractor::SceneRepresentative,
KeyframeExtractor::Manual,
] {
assert_eq!(KeyframeExtractor::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(KeyframeExtractor::from_str("iframe"), None);
assert_eq!(KeyframeExtractor::from_str("not_a_slug"), None);
}
#[test]
fn subtitle_kind_slug_roundtrip() {
for v in [
SubtitleKind::FullDialogue,
SubtitleKind::ForcedNarrative,
SubtitleKind::CommentaryText,
] {
assert_eq!(SubtitleKind::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(SubtitleKind::from_str("not_a_slug"), None);
}
#[test]
fn audio_content_kind_slug_roundtrip() {
for v in [
AudioContentKind::Speech,
AudioContentKind::Music,
AudioContentKind::Mixed,
AudioContentKind::Silence,
] {
assert_eq!(AudioContentKind::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(AudioContentKind::from_str("not_a_slug"), None);
}
#[test]
fn scan_status_slug_roundtrip() {
for v in [ScanStatus::Ok, ScanStatus::Partial, ScanStatus::Failed] {
assert_eq!(ScanStatus::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(ScanStatus::from_str("not_a_slug"), None);
}
#[test]
fn video_index_stage_slug_roundtrip() {
for v in [
VideoIndexStage::Pending,
VideoIndexStage::Probed,
VideoIndexStage::SceneDetected,
VideoIndexStage::KeyframeExtracted,
VideoIndexStage::Analyzed,
VideoIndexStage::Embedded,
VideoIndexStage::Done,
VideoIndexStage::Failed,
] {
assert_eq!(VideoIndexStage::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(VideoIndexStage::from_str("not_a_slug"), None);
}
#[test]
fn audio_index_stage_slug_roundtrip() {
for v in [
AudioIndexStage::Pending,
AudioIndexStage::Extracted,
AudioIndexStage::Analyzed,
AudioIndexStage::Transcribed,
AudioIndexStage::Diarized,
AudioIndexStage::Embedded,
AudioIndexStage::Done,
AudioIndexStage::Failed,
] {
assert_eq!(AudioIndexStage::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(AudioIndexStage::from_str("not_a_slug"), None);
}
#[test]
fn subtitle_index_stage_slug_roundtrip() {
for v in [
SubtitleIndexStage::Pending,
SubtitleIndexStage::TracksDiscovered,
SubtitleIndexStage::CuesExtracted,
SubtitleIndexStage::Ocr,
SubtitleIndexStage::SearchIndexed,
SubtitleIndexStage::Done,
SubtitleIndexStage::Failed,
] {
assert_eq!(SubtitleIndexStage::from_str(v.as_str()), Some(v), "{v:?}");
}
assert_eq!(SubtitleIndexStage::from_str("not_a_slug"), None);
}
}
#[cfg(all(test, any(feature = "std", feature = "alloc")))]
mod tests {
use super::*;
#[test]
fn media_kind_default_video() {
assert_eq!(MediaKind::default(), MediaKind::Video);
}
#[test]
fn keyframe_extractor_superset_of_scene_detector() {
let _ = SceneDetector::Histogram;
let _ = KeyframeExtractor::Histogram;
let _ = KeyframeExtractor::Phash;
let _ = KeyframeExtractor::Threshold;
let _ = KeyframeExtractor::Content;
let _ = KeyframeExtractor::Adaptive;
let _ = KeyframeExtractor::Manual;
let _ = KeyframeExtractor::CompositeQuality;
let _ = KeyframeExtractor::Interval;
let _ = KeyframeExtractor::IFrame;
let _ = KeyframeExtractor::SceneRepresentative;
}
fn probe_error() -> std::vec::Vec<ErrorInfo> {
std::vec![ErrorInfo::code_only(ErrorCode::ProbeCorrupt)]
}
#[test]
fn video_index_stage_progression() {
use VideoIndexStatus as S;
let no_err: &[ErrorInfo] = &[];
assert_eq!(
VideoIndexStage::from_status(S::empty(), no_err),
VideoIndexStage::Pending
);
assert_eq!(
VideoIndexStage::from_status(S::PROBED, no_err),
VideoIndexStage::Probed
);
assert_eq!(
VideoIndexStage::from_status(S::PROBED | S::SCENE_DETECTED, no_err),
VideoIndexStage::SceneDetected
);
assert_eq!(
VideoIndexStage::from_status(
S::PROBED | S::SCENE_DETECTED | S::KEYFRAME_EXTRACTED,
no_err
),
VideoIndexStage::KeyframeExtracted
);
assert_eq!(
VideoIndexStage::from_status(
S::PROBED | S::SCENE_DETECTED | S::KEYFRAME_EXTRACTED | S::VLM_ANALYZED,
no_err
),
VideoIndexStage::Analyzed
);
assert_eq!(
VideoIndexStage::from_status(
S::PROBED
| S::SCENE_DETECTED
| S::KEYFRAME_EXTRACTED
| S::APPLE_VISION_ANALYZED
| S::TEXT_EMBEDDING_FINISHED,
no_err
),
VideoIndexStage::Embedded
);
assert_eq!(
VideoIndexStage::from_status(S::fully_indexed_mask(), no_err),
VideoIndexStage::Done
);
}
#[test]
fn video_index_stage_contiguous_walk_does_not_jump_on_out_of_order_bits() {
use VideoIndexStatus as S;
let no_err: &[ErrorInfo] = &[];
let stray = S::TEXT_EMBEDDING_FINISHED; assert_eq!(
VideoIndexStage::from_status(stray, no_err),
VideoIndexStage::Pending,
"stray late bit must not advance stage"
);
assert_eq!(
VideoIndexStage::from_status(S::PROBED | S::TEXT_EMBEDDING_FINISHED, no_err),
VideoIndexStage::Probed,
);
}
#[test]
fn video_index_stage_failed_precedence_on_live_errors_only() {
use VideoIndexStatus as S;
assert_eq!(
VideoIndexStage::from_status(S::empty(), &probe_error()),
VideoIndexStage::Failed
);
assert_eq!(
VideoIndexStage::from_status(S::PROBED, &probe_error()),
VideoIndexStage::Probed,
"successful retry must clear stale stage errors"
);
let mixed = std::vec![
ErrorInfo::code_only(ErrorCode::ProbeCorrupt),
ErrorInfo::code_only(ErrorCode::SceneDetectionFailed),
];
assert_eq!(
VideoIndexStage::from_status(S::PROBED, &mixed),
VideoIndexStage::Failed,
);
}
#[test]
fn audio_index_stage_progression() {
use AudioIndexStatus as S;
let no_err: &[ErrorInfo] = &[];
assert_eq!(
AudioIndexStage::from_status(S::empty(), no_err),
AudioIndexStage::Pending
);
assert_eq!(
AudioIndexStage::from_status(S::EXTRACTED, no_err),
AudioIndexStage::Extracted
);
assert_eq!(
AudioIndexStage::from_status(S::EXTRACTED | S::VAD_DONE, no_err),
AudioIndexStage::Analyzed
);
assert_eq!(
AudioIndexStage::from_status(
S::EXTRACTED | S::CLASSIFIED | S::VAD_DONE | S::STT_DONE,
no_err
),
AudioIndexStage::Transcribed
);
assert_eq!(
AudioIndexStage::from_status(
S::EXTRACTED | S::CLASSIFIED | S::VAD_DONE | S::STT_DONE | S::SPEAKER_DONE,
no_err
),
AudioIndexStage::Diarized
);
assert_eq!(
AudioIndexStage::from_status(S::fully_indexed_mask(), no_err),
AudioIndexStage::Done
);
assert_eq!(
AudioIndexStage::from_status(S::empty(), &probe_error()),
AudioIndexStage::Failed
);
}
#[test]
fn audio_index_stage_contiguous_walk() {
use AudioIndexStatus as S;
let no_err: &[ErrorInfo] = &[];
assert_eq!(
AudioIndexStage::from_status(S::STT_DONE, no_err),
AudioIndexStage::Pending,
);
}
#[test]
fn subtitle_text_track_reaches_done_without_ocr() {
use SubtitleIndexStatus as S;
let no_err: &[ErrorInfo] = &[];
let status = S::TRACKS_DISCOVERED | S::CUES_EXTRACTED | S::SEARCH_INDEXED;
assert_eq!(
SubtitleIndexStage::from_status(status, false, no_err),
SubtitleIndexStage::Done,
);
assert_ne!(
SubtitleIndexStage::from_status(status, true, no_err),
SubtitleIndexStage::Done,
);
}
#[test]
fn subtitle_index_stage_progression() {
use SubtitleIndexStatus as S;
let no_err: &[ErrorInfo] = &[];
assert_eq!(
SubtitleIndexStage::from_status(S::empty(), true, no_err),
SubtitleIndexStage::Pending
);
assert_eq!(
SubtitleIndexStage::from_status(S::TRACKS_DISCOVERED, true, no_err),
SubtitleIndexStage::TracksDiscovered
);
assert_eq!(
SubtitleIndexStage::from_status(S::TRACKS_DISCOVERED | S::CUES_EXTRACTED, true, no_err),
SubtitleIndexStage::CuesExtracted
);
assert_eq!(
SubtitleIndexStage::from_status(
S::TRACKS_DISCOVERED | S::CUES_EXTRACTED | S::OCR_DONE,
true,
no_err
),
SubtitleIndexStage::Ocr
);
assert_eq!(
SubtitleIndexStage::from_status(S::fully_indexed_mask(true), true, no_err),
SubtitleIndexStage::Done
);
assert_eq!(
SubtitleIndexStage::from_status(S::empty(), true, &probe_error()),
SubtitleIndexStage::Failed
);
}
}