1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SessionState {
13 Pending,
15 Running,
17 Completed,
19 Failed,
21 Cancelled,
23}
24
25impl SessionState {
26 #[must_use]
28 pub fn is_active(self) -> bool {
29 self == SessionState::Running
30 }
31
32 #[must_use]
34 pub fn is_terminal(self) -> bool {
35 matches!(
36 self,
37 SessionState::Completed | SessionState::Failed | SessionState::Cancelled
38 )
39 }
40
41 #[must_use]
43 pub fn label(self) -> &'static str {
44 match self {
45 SessionState::Pending => "pending",
46 SessionState::Running => "running",
47 SessionState::Completed => "completed",
48 SessionState::Failed => "failed",
49 SessionState::Cancelled => "cancelled",
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct TranscodeSession {
57 pub id: u64,
59 pub input_path: String,
61 pub output_path: String,
63 pub state: SessionState,
65 pub start_ms: u64,
67 pub end_ms: u64,
69 progress: f64,
71 pub total_duration_ms: u64,
73}
74
75impl TranscodeSession {
76 pub fn new(
78 id: u64,
79 input_path: impl Into<String>,
80 output_path: impl Into<String>,
81 total_duration_ms: u64,
82 ) -> Self {
83 Self {
84 id,
85 input_path: input_path.into(),
86 output_path: output_path.into(),
87 state: SessionState::Pending,
88 start_ms: 0,
89 end_ms: 0,
90 progress: 0.0,
91 total_duration_ms,
92 }
93 }
94
95 pub fn start(&mut self, now_ms: u64) {
97 self.state = SessionState::Running;
98 self.start_ms = now_ms;
99 }
100
101 pub fn set_progress(&mut self, pct: f64) {
103 self.progress = pct.clamp(0.0, 1.0);
104 }
105
106 pub fn complete(&mut self, now_ms: u64) {
108 self.state = SessionState::Completed;
109 self.end_ms = now_ms;
110 self.progress = 1.0;
111 }
112
113 pub fn fail(&mut self, now_ms: u64) {
115 self.state = SessionState::Failed;
116 self.end_ms = now_ms;
117 }
118
119 pub fn cancel(&mut self, now_ms: u64) {
121 self.state = SessionState::Cancelled;
122 self.end_ms = now_ms;
123 }
124
125 #[must_use]
127 pub fn elapsed_ms(&self, now_ms: u64) -> u64 {
128 if self.start_ms == 0 {
129 return 0;
130 }
131 let end = if self.state.is_terminal() {
132 self.end_ms
133 } else {
134 now_ms
135 };
136 end.saturating_sub(self.start_ms)
137 }
138
139 #[allow(clippy::cast_precision_loss)]
141 #[must_use]
142 pub fn progress_pct(&self) -> f64 {
143 self.progress * 100.0
144 }
145
146 #[must_use]
148 pub fn is_active(&self) -> bool {
149 self.state.is_active()
150 }
151}
152
153#[derive(Debug, Default)]
155pub struct TranscodeSessionManager {
156 sessions: HashMap<u64, TranscodeSession>,
157 next_id: u64,
158}
159
160impl TranscodeSessionManager {
161 #[must_use]
163 pub fn new() -> Self {
164 Self::default()
165 }
166
167 pub fn create(
169 &mut self,
170 input_path: impl Into<String>,
171 output_path: impl Into<String>,
172 total_duration_ms: u64,
173 ) -> u64 {
174 let id = self.next_id;
175 self.next_id += 1;
176 let session = TranscodeSession::new(id, input_path, output_path, total_duration_ms);
177 self.sessions.insert(id, session);
178 id
179 }
180
181 #[must_use]
183 pub fn get(&self, id: u64) -> Option<&TranscodeSession> {
184 self.sessions.get(&id)
185 }
186
187 pub fn get_mut(&mut self, id: u64) -> Option<&mut TranscodeSession> {
189 self.sessions.get_mut(&id)
190 }
191
192 #[must_use]
194 pub fn active_count(&self) -> usize {
195 self.sessions.values().filter(|s| s.is_active()).count()
196 }
197
198 #[must_use]
200 pub fn total_count(&self) -> usize {
201 self.sessions.len()
202 }
203
204 pub fn remove(&mut self, id: u64) -> bool {
206 self.sessions.remove(&id).is_some()
207 }
208
209 #[must_use]
211 pub fn sessions_in_state(&self, state: SessionState) -> Vec<u64> {
212 self.sessions
213 .values()
214 .filter(|s| s.state == state)
215 .map(|s| s.id)
216 .collect()
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_state_is_active_only_running() {
226 assert!(SessionState::Running.is_active());
227 assert!(!SessionState::Pending.is_active());
228 assert!(!SessionState::Completed.is_active());
229 assert!(!SessionState::Failed.is_active());
230 assert!(!SessionState::Cancelled.is_active());
231 }
232
233 #[test]
234 fn test_state_is_terminal() {
235 assert!(SessionState::Completed.is_terminal());
236 assert!(SessionState::Failed.is_terminal());
237 assert!(SessionState::Cancelled.is_terminal());
238 assert!(!SessionState::Pending.is_terminal());
239 assert!(!SessionState::Running.is_terminal());
240 }
241
242 #[test]
243 fn test_state_labels() {
244 assert_eq!(SessionState::Running.label(), "running");
245 assert_eq!(SessionState::Pending.label(), "pending");
246 assert_eq!(SessionState::Completed.label(), "completed");
247 }
248
249 #[test]
250 fn test_session_initial_state_pending() {
251 let s = TranscodeSession::new(0, "in.mp4", "out.mp4", 60_000);
252 assert_eq!(s.state, SessionState::Pending);
253 assert_eq!(s.elapsed_ms(1000), 0);
254 }
255
256 #[test]
257 fn test_session_start_sets_running() {
258 let mut s = TranscodeSession::new(0, "in", "out", 60_000);
259 s.start(1000);
260 assert!(s.is_active());
261 assert_eq!(s.state, SessionState::Running);
262 }
263
264 #[test]
265 fn test_session_elapsed_ms_while_running() {
266 let mut s = TranscodeSession::new(0, "in", "out", 60_000);
267 s.start(1000);
268 assert_eq!(s.elapsed_ms(4000), 3000);
269 }
270
271 #[test]
272 fn test_session_elapsed_ms_after_complete() {
273 let mut s = TranscodeSession::new(0, "in", "out", 60_000);
274 s.start(1000);
275 s.complete(5000);
276 assert_eq!(s.elapsed_ms(9999), 4000);
278 }
279
280 #[test]
281 fn test_session_progress_pct_clamped() {
282 let mut s = TranscodeSession::new(0, "in", "out", 60_000);
283 s.set_progress(1.5);
284 assert!((s.progress_pct() - 100.0).abs() < f64::EPSILON);
285 s.set_progress(-0.5);
286 assert!((s.progress_pct()).abs() < f64::EPSILON);
287 }
288
289 #[test]
290 fn test_session_complete_sets_progress_full() {
291 let mut s = TranscodeSession::new(0, "in", "out", 60_000);
292 s.start(0);
293 s.complete(1000);
294 assert!((s.progress_pct() - 100.0).abs() < f64::EPSILON);
295 }
296
297 #[test]
298 fn test_session_fail() {
299 let mut s = TranscodeSession::new(0, "in", "out", 60_000);
300 s.start(0);
301 s.fail(500);
302 assert_eq!(s.state, SessionState::Failed);
303 assert!(s.state.is_terminal());
304 }
305
306 #[test]
307 fn test_manager_create_and_get() {
308 let mut mgr = TranscodeSessionManager::new();
309 let id = mgr.create("in.mp4", "out.mp4", 60_000);
310 let s = mgr.get(id).expect("should succeed in test");
311 assert_eq!(s.id, id);
312 assert_eq!(s.state, SessionState::Pending);
313 }
314
315 #[test]
316 fn test_manager_active_count() {
317 let mut mgr = TranscodeSessionManager::new();
318 let id1 = mgr.create("a", "b", 1000);
319 let id2 = mgr.create("c", "d", 1000);
320 mgr.get_mut(id1).expect("should succeed in test").start(0);
321 assert_eq!(mgr.active_count(), 1);
322 mgr.get_mut(id2).expect("should succeed in test").start(0);
323 assert_eq!(mgr.active_count(), 2);
324 }
325
326 #[test]
327 fn test_manager_remove() {
328 let mut mgr = TranscodeSessionManager::new();
329 let id = mgr.create("in", "out", 1000);
330 assert!(mgr.remove(id));
331 assert!(!mgr.remove(id));
332 assert!(mgr.get(id).is_none());
333 }
334
335 #[test]
336 fn test_manager_sessions_in_state() {
337 let mut mgr = TranscodeSessionManager::new();
338 let id = mgr.create("in", "out", 1000);
339 mgr.get_mut(id).expect("should succeed in test").start(0);
340 mgr.get_mut(id)
341 .expect("should succeed in test")
342 .complete(100);
343 let completed = mgr.sessions_in_state(SessionState::Completed);
344 assert!(completed.contains(&id));
345 }
346}