1use std::fmt;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use crate::download::DownloadPriority;
6use crate::model::Video;
7use crate::model::chapter::Chapter;
8use crate::model::format::Format;
9use crate::model::playlist::Playlist;
10
11#[cfg(feature = "live-recording")]
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
14pub enum RecordingMethod {
15 Native,
17 Fallback,
19}
20
21#[derive(Debug, Clone, serde::Serialize)]
23#[allow(clippy::large_enum_variant)]
24pub enum DownloadEvent {
25 VideoFetched {
27 url: String,
28 video: Box<Video>,
29 duration: Duration,
30 },
31
32 VideoFetchFailed {
34 url: String,
35 error: String,
36 duration: Duration,
37 },
38
39 DownloadQueued {
41 download_id: u64,
42 url: String,
43 priority: DownloadPriority,
44 output_path: PathBuf,
45 },
46
47 DownloadStarted {
49 download_id: u64,
50 url: String,
51 total_bytes: u64,
52 format_id: Option<String>,
53 },
54
55 DownloadProgress {
57 download_id: u64,
58 downloaded_bytes: u64,
59 total_bytes: u64,
60 speed_bytes_per_sec: f64,
61 eta_seconds: Option<u64>,
62 },
63
64 DownloadPaused { download_id: u64, reason: String },
66
67 DownloadResumed { download_id: u64 },
69
70 DownloadCompleted {
72 download_id: u64,
73 url: String,
74 output_path: PathBuf,
75 duration: Duration,
76 total_bytes: u64,
77 },
78
79 DownloadFailed {
81 download_id: u64,
82 url: String,
83 error: String,
84 retry_count: u32,
85 },
86
87 DownloadCanceled { download_id: u64, reason: String },
89
90 FormatSelected {
92 video_id: String,
93 format: Format,
94 quality: String,
95 },
96
97 MetadataApplied { path: PathBuf, metadata_type: MetadataType },
99
100 ChaptersEmbedded { path: PathBuf, chapters: Vec<Chapter> },
102
103 PostProcessStarted {
105 input_path: PathBuf,
106 operation: PostProcessOperation,
107 },
108
109 PostProcessCompleted {
111 input_path: PathBuf,
112 output_path: PathBuf,
113 operation: PostProcessOperation,
114 duration: Duration,
115 },
116
117 PostProcessFailed {
119 input_path: PathBuf,
120 operation: PostProcessOperation,
121 error: String,
122 },
123
124 PlaylistFetched {
126 url: String,
127 playlist: Playlist,
128 duration: Duration,
129 },
130
131 PlaylistFetchFailed {
133 url: String,
134 error: String,
135 duration: Duration,
136 },
137
138 PlaylistItemStarted {
140 playlist_id: String,
141 index: usize,
142 total: usize,
143 video_id: String,
144 },
145
146 PlaylistItemCompleted {
148 playlist_id: String,
149 index: usize,
150 total: usize,
151 video_id: String,
152 output_path: PathBuf,
153 },
154
155 PlaylistItemFailed {
157 playlist_id: String,
158 index: usize,
159 total: usize,
160 video_id: String,
161 error: String,
162 },
163
164 PlaylistCompleted {
166 playlist_id: String,
167 total_items: usize,
168 successful: usize,
169 failed: usize,
170 duration: Duration,
171 },
172
173 SegmentStarted {
175 download_id: u64,
176 segment_index: usize,
177 total_segments: usize,
178 },
179
180 SegmentCompleted {
182 download_id: u64,
183 segment_index: usize,
184 total_segments: usize,
185 bytes: u64,
186 },
187
188 #[cfg(feature = "live-recording")]
190 LiveRecordingStarted {
191 video_id: String,
192 url: String,
193 quality: String,
194 method: RecordingMethod,
195 },
196
197 #[cfg(feature = "live-recording")]
199 LiveRecordingProgress {
200 video_id: String,
201 elapsed: Duration,
202 bytes_written: u64,
203 segments: u64,
204 bitrate_bps: f64,
205 },
206
207 #[cfg(feature = "live-recording")]
209 LiveRecordingStopped {
210 video_id: String,
211 reason: String,
212 output_path: PathBuf,
213 total_bytes: u64,
214 total_duration: Duration,
215 },
216
217 #[cfg(feature = "live-recording")]
219 LiveRecordingFailed { video_id: String, error: String },
220
221 #[cfg(feature = "live-streaming")]
223 LiveStreamStarted {
224 video_id: String,
225 url: String,
226 quality: String,
227 },
228
229 #[cfg(feature = "live-streaming")]
231 LiveStreamProgress {
232 video_id: String,
233 elapsed: Duration,
234 bytes_received: u64,
235 segments: u64,
236 bitrate_bps: f64,
237 },
238
239 #[cfg(feature = "live-streaming")]
241 LiveStreamStopped {
242 video_id: String,
243 reason: String,
244 total_bytes: u64,
245 total_duration: Duration,
246 },
247
248 #[cfg(feature = "live-streaming")]
250 LiveStreamFailed { video_id: String, error: String },
251}
252
253#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)]
255pub enum MetadataType {
256 Mp3,
258 Mp4,
260 Ffmpeg,
262}
263
264impl fmt::Display for MetadataType {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 match self {
267 Self::Mp3 => f.write_str("Mp3"),
268 Self::Mp4 => f.write_str("Mp4"),
269 Self::Ffmpeg => f.write_str("Ffmpeg"),
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
276pub enum PostProcessOperation {
277 CombineStreams { audio_path: PathBuf, video_path: PathBuf },
279 ConvertAudio { target_format: String },
281 EmbedSubtitles { subtitle_path: PathBuf },
283 EmbedThumbnail { thumbnail_path: PathBuf },
285 Custom { description: String },
287 SplitChapters { source_path: PathBuf, chapter_count: usize },
289}
290
291impl fmt::Display for PostProcessOperation {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 match self {
294 Self::CombineStreams { .. } => f.write_str("CombineStreams"),
295 Self::ConvertAudio { target_format } => {
296 write!(f, "ConvertAudio(format={})", target_format)
297 }
298 Self::EmbedSubtitles { .. } => f.write_str("EmbedSubtitles"),
299 Self::EmbedThumbnail { .. } => f.write_str("EmbedThumbnail"),
300 Self::Custom { description } => write!(f, "Custom(description={})", description),
301 Self::SplitChapters { chapter_count, .. } => write!(f, "SplitChapters(chapters={})", chapter_count),
302 }
303 }
304}
305
306#[cfg(feature = "live-recording")]
307impl fmt::Display for RecordingMethod {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 match self {
310 Self::Native => f.write_str("Native"),
311 Self::Fallback => f.write_str("Fallback"),
312 }
313 }
314}
315
316impl DownloadEvent {
317 pub fn download_id(&self) -> Option<u64> {
319 match self {
320 Self::DownloadQueued { download_id, .. }
321 | Self::DownloadStarted { download_id, .. }
322 | Self::DownloadProgress { download_id, .. }
323 | Self::DownloadPaused { download_id, .. }
324 | Self::DownloadResumed { download_id, .. }
325 | Self::DownloadCompleted { download_id, .. }
326 | Self::DownloadFailed { download_id, .. }
327 | Self::DownloadCanceled { download_id, .. }
328 | Self::SegmentStarted { download_id, .. }
329 | Self::SegmentCompleted { download_id, .. } => Some(*download_id),
330 _ => None,
331 }
332 }
333
334 pub fn is_terminal(&self) -> bool {
336 matches!(
337 self,
338 Self::DownloadCompleted { .. } | Self::DownloadFailed { .. } | Self::DownloadCanceled { .. }
339 )
340 }
341
342 pub fn is_progress(&self) -> bool {
344 match self {
345 Self::DownloadProgress { .. } => true,
346 #[cfg(feature = "live-recording")]
347 Self::LiveRecordingProgress { .. } => true,
348 #[cfg(feature = "live-streaming")]
349 Self::LiveStreamProgress { .. } => true,
350 _ => false,
351 }
352 }
353
354 pub fn event_type(&self) -> &'static str {
356 match self {
357 Self::VideoFetched { .. } => "video_fetched",
358 Self::VideoFetchFailed { .. } => "video_fetch_failed",
359 Self::DownloadQueued { .. } => "download_queued",
360 Self::DownloadStarted { .. } => "download_started",
361 Self::DownloadProgress { .. } => "download_progress",
362 Self::DownloadPaused { .. } => "download_paused",
363 Self::DownloadResumed { .. } => "download_resumed",
364 Self::DownloadCompleted { .. } => "download_completed",
365 Self::DownloadFailed { .. } => "download_failed",
366 Self::DownloadCanceled { .. } => "download_canceled",
367 Self::FormatSelected { .. } => "format_selected",
368 Self::MetadataApplied { .. } => "metadata_applied",
369 Self::ChaptersEmbedded { .. } => "chapters_embedded",
370 Self::PostProcessStarted { .. } => "post_process_started",
371 Self::PostProcessCompleted { .. } => "post_process_completed",
372 Self::PostProcessFailed { .. } => "post_process_failed",
373 Self::PlaylistFetched { .. } => "playlist_fetched",
374 Self::PlaylistFetchFailed { .. } => "playlist_fetch_failed",
375 Self::PlaylistItemStarted { .. } => "playlist_item_started",
376 Self::PlaylistItemCompleted { .. } => "playlist_item_completed",
377 Self::PlaylistItemFailed { .. } => "playlist_item_failed",
378 Self::PlaylistCompleted { .. } => "playlist_completed",
379 Self::SegmentStarted { .. } => "segment_started",
380 Self::SegmentCompleted { .. } => "segment_completed",
381 #[cfg(feature = "live-recording")]
382 Self::LiveRecordingStarted { .. } => "live_recording_started",
383 #[cfg(feature = "live-recording")]
384 Self::LiveRecordingProgress { .. } => "live_recording_progress",
385 #[cfg(feature = "live-recording")]
386 Self::LiveRecordingStopped { .. } => "live_recording_stopped",
387 #[cfg(feature = "live-recording")]
388 Self::LiveRecordingFailed { .. } => "live_recording_failed",
389 #[cfg(feature = "live-streaming")]
390 Self::LiveStreamStarted { .. } => "live_stream_started",
391 #[cfg(feature = "live-streaming")]
392 Self::LiveStreamProgress { .. } => "live_stream_progress",
393 #[cfg(feature = "live-streaming")]
394 Self::LiveStreamStopped { .. } => "live_stream_stopped",
395 #[cfg(feature = "live-streaming")]
396 Self::LiveStreamFailed { .. } => "live_stream_failed",
397 }
398 }
399}
400
401impl fmt::Display for DownloadEvent {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 Self::DownloadProgress {
405 download_id,
406 downloaded_bytes,
407 total_bytes,
408 ..
409 } => {
410 write!(
411 f,
412 "DownloadProgress(id={}, downloaded={}, total={})",
413 download_id, downloaded_bytes, total_bytes
414 )
415 }
416 _ => {
417 let event_type = self.event_type();
418 if let Some(id) = self.download_id() {
419 write!(f, "{}(id={})", event_type, id)
420 } else {
421 f.write_str(event_type)
422 }
423 }
424 }
425 }
426}