use myko::prelude::*;
use myko::TS;
use myko_macros::myko_item;
use serde::{Deserialize, Serialize};
use crate::{
Capture, CaptureIntegrity, CaptureQuality, CollectionPathSegment, CreativeStatus,
DeliveryStatus, QcStatus, RecordingJob, RecordingJobId, RecordingJobPhase, Representation,
RepresentationKind, ShotDirection, ShotEntryPlan, Take, TakeState,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
pub enum LegacyCaptureAttemptState {
#[default]
Recording,
Recorded,
QualityWarning,
RetryingSourceLoss,
ReshootRequired,
Failed,
Delivered,
Accepted,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct LegacyCaptureAttempt {
pub attempt_id: String,
#[serde(default, alias = "cueId")]
pub clip_id: String,
pub shot_id: String,
#[serde(default)]
pub shot_name: String,
pub direction: ShotDirection,
pub attempt_number: u32,
pub state: LegacyCaptureAttemptState,
#[serde(default)]
pub quality: CaptureQuality,
#[serde(default)]
pub file_name: String,
#[serde(default)]
pub error: String,
#[serde(default)]
pub started_at_ms: u64,
#[serde(default)]
pub updated_at_ms: u64,
}
#[myko_item]
pub struct CaptureRun {
pub run_id: String,
pub streamer_id: String,
#[serde(default, alias = "shotListId")]
pub timeline_id: String,
#[serde(default, alias = "shotListName")]
pub timeline_name: String,
#[serde(default, alias = "shotListVersion")]
pub timeline_version: u32,
#[serde(default)]
pub collection_id: String,
#[serde(default)]
pub collection_path: Vec<CollectionPathSegment>,
#[serde(default)]
pub phase: RecordingJobPhase,
#[serde(default, alias = "items")]
pub shots: Vec<ShotEntryPlan>,
#[serde(default)]
pub attempts: Vec<LegacyCaptureAttempt>,
#[serde(default)]
pub error: String,
#[serde(default)]
pub started_at_ms: u64,
#[serde(default)]
pub updated_at_ms: u64,
#[serde(default)]
pub elapsed_ms: u64,
#[serde(default)]
pub estimated_total_ms: u64,
#[serde(default)]
pub estimated_remaining_ms: u64,
}
impl CaptureRun {
pub fn to_recording_job(&self) -> RecordingJob {
let job_id = if self.run_id.trim().is_empty() {
self.id.to_string()
} else {
self.run_id.clone()
};
RecordingJob {
id: RecordingJobId::from(job_id.clone()),
job_id,
capture_kind: crate::RecordingKind::Video,
streamer_id: self.streamer_id.clone(),
capture_context: None,
timeline_id: self.timeline_id.clone(),
timeline_name: self.timeline_name.clone(),
timeline_revision: self.timeline_version,
collection_id: self.collection_id.clone(),
collection_path: self.collection_path.clone(),
phase: self.phase.clone(),
pause_requested: false,
entries: self.shots.clone(),
takes: self.attempts.iter().map(legacy_take).collect(),
error: self.error.clone(),
started_at_ms: self.started_at_ms,
updated_at_ms: self.updated_at_ms,
elapsed_ms: self.elapsed_ms,
estimated_total_ms: self.estimated_total_ms,
estimated_remaining_ms: self.estimated_remaining_ms,
}
}
}
fn legacy_take(attempt: &LegacyCaptureAttempt) -> Take {
let state = match attempt.state {
LegacyCaptureAttemptState::Recording => TakeState::Recording,
LegacyCaptureAttemptState::Failed => TakeState::Failed,
_ => TakeState::Completed,
};
let has_capture = !matches!(
attempt.state,
LegacyCaptureAttemptState::Recording | LegacyCaptureAttemptState::Failed
) || !attempt.file_name.trim().is_empty();
let capture = has_capture.then(|| {
let integrity = if !attempt.quality.structurally_valid {
CaptureIntegrity::Corrupt
} else if attempt.quality.source_transport_damage {
CaptureIntegrity::Damaged
} else {
CaptureIntegrity::Complete
};
let qc_status = if matches!(integrity, CaptureIntegrity::Corrupt)
|| attempt.state == LegacyCaptureAttemptState::ReshootRequired
{
QcStatus::Failed
} else if attempt.quality.advisory {
QcStatus::Warning
} else {
QcStatus::Clean
};
let delivery_status = if matches!(
attempt.state,
LegacyCaptureAttemptState::Delivered
| LegacyCaptureAttemptState::Accepted
| LegacyCaptureAttemptState::ReshootRequired
) {
DeliveryStatus::Delivered
} else {
DeliveryStatus::Pending
};
let representations = (!attempt.file_name.trim().is_empty())
.then(|| Representation {
representation_id: format!("{}:delivery", attempt.attempt_id),
kind: RepresentationKind::Delivery,
codec: "h264".to_owned(),
container: "mp4".to_owned(),
file_name: attempt.file_name.clone(),
})
.into_iter()
.collect();
Capture {
capture_id: format!("{}:capture", attempt.attempt_id),
integrity,
qc_status,
creative_status: if attempt.state == LegacyCaptureAttemptState::Accepted {
CreativeStatus::Accepted
} else {
CreativeStatus::Unreviewed
},
delivery_status,
quality: attempt.quality.clone(),
representations,
}
});
Take {
take_id: attempt.attempt_id.clone(),
entry_id: attempt.clip_id.clone(),
shot_id: attempt.shot_id.clone(),
shot_name: attempt.shot_name.clone(),
direction: attempt.direction,
take_number: attempt.attempt_number,
state,
capture,
error: attempt.error.clone(),
started_at_ms: attempt.started_at_ms,
updated_at_ms: attempt.updated_at_ms,
}
}