1use std::path::PathBuf;
7use std::time::Duration;
8
9use thiserror::Error;
10
11use crate::model::format::FormatType;
12use crate::utils::platform::{Architecture, Platform};
13
14pub type Result<T> = std::result::Result<T, Error>;
16
17#[derive(Debug, Error)]
22pub enum Error {
23 #[error("Async task failed: {context}")]
28 Runtime {
29 context: String,
30 #[source]
31 source: tokio::task::JoinError,
32 },
33
34 #[error("IO error during {operation}")]
38 IO {
39 operation: String,
40 path: Option<PathBuf>,
41 #[source]
42 source: std::io::Error,
43 },
44
45 #[error("Failed to extract archive {file}: {source}")]
49 Archive {
50 file: String,
51 #[source]
52 source: ArchiveError,
53 },
54
55 #[error("HTTP request failed for {url}: {context}")]
60 Http {
61 url: String,
62 context: String,
63 #[source]
64 source: reqwest::Error,
65 },
66
67 #[error("Timeout after {duration:?} while {operation}")]
71 Timeout { operation: String, duration: Duration },
72
73 #[error("JSON error while {context}: {source}")]
78 Json {
79 context: String,
80 #[source]
81 source: serde_json::Error,
82 },
83
84 #[cfg(feature = "cache-redb")]
88 #[error("Database error during {operation}: {source}")]
89 Database {
90 operation: String,
91 #[source]
92 source: Box<redb::Error>,
93 },
94
95 #[cfg(feature = "cache-redis")]
99 #[error("Redis error during {operation}: {source}")]
100 Redis {
101 operation: String,
102 #[source]
103 source: redis::RedisError,
104 },
105
106 #[error("No {binary} release found for {platform}/{architecture}")]
111 NoBinaryRelease {
112 binary: String,
113 platform: Platform,
114 architecture: Architecture,
115 },
116
117 #[error("{binary} binary not found at {path} after installation")]
121 BinaryNotFound { binary: String, path: PathBuf },
122
123 #[error("Command '{command}' failed (exit code: {exit_code}): {stderr}")]
127 CommandFailed {
128 command: String,
129 exit_code: i32,
130 stderr: String,
131 },
132
133 #[error("Failed to fetch video from {url}: {reason}")]
138 VideoFetch { url: String, reason: String },
139
140 #[error("Video {video_id} is missing required field: {field}")]
144 VideoMissingField { video_id: String, field: String },
145
146 #[error("No {format_type} format available for video {video_id}")]
150 FormatNotAvailable {
151 video_id: String,
152 format_type: FormatType,
153 available_formats: Vec<String>,
154 },
155
156 #[error("Format {format_id} for video {video_id} has no download URL")]
160 FormatNoUrl { video_id: String, format_id: String },
161
162 #[error("Format {format_id} is incompatible: {reason}")]
166 FormatIncompatible { format_id: String, reason: String },
167
168 #[error("No thumbnail available for video {video_id}")]
170 NoThumbnail { video_id: String },
171
172 #[error("No subtitles available for video {video_id} in language '{language}'")]
174 SubtitleNotAvailable { video_id: String, language: String },
175
176 #[error("URL expired")]
178 UrlExpired,
179
180 #[error("Invalid path '{path}': {reason}")]
185 PathValidation { path: PathBuf, reason: String },
186
187 #[error("Invalid URL '{url}': {reason}")]
191 UrlValidation { url: String, reason: String },
192
193 #[error("Download {download_id} failed: {reason}")]
198 DownloadFailed { download_id: u64, reason: String },
199
200 #[error("Download {download_id} was cancelled")]
202 DownloadCancelled { download_id: u64 },
203
204 #[error("Invalid partial range: {reason}")]
206 InvalidPartialRange { reason: String },
207
208 #[cfg(any(feature = "live-recording", feature = "live-streaming"))]
211 #[error("Video at {url} is not live (status={live_status}): {reason}")]
212 LiveStreamUnavailable {
213 url: String,
214 live_status: String,
215 reason: String,
216 },
217
218 #[cfg(any(feature = "live-recording", feature = "live-streaming"))]
220 #[error("HLS parsing failed for {url}: {context}")]
221 HlsParsing { url: String, context: String },
222
223 #[cfg(feature = "live-recording")]
225 #[error("Live recording failed for {url}: {reason}")]
226 LiveRecording { url: String, reason: String },
227
228 #[cfg(feature = "live-streaming")]
230 #[error("Live streaming failed for {url}: {reason}")]
231 LiveStreaming { url: String, reason: String },
232
233 #[error("Metadata {operation} failed for {path}: {reason}")]
238 Metadata {
239 operation: String,
240 path: PathBuf,
241 reason: String,
242 },
243
244 #[error("Cache miss for {key}")]
247 CacheMiss { key: String },
248
249 #[error("Cache entry expired for {key}")]
251 CacheExpired { key: String },
252
253 #[cfg(persistent_cache)]
258 #[error(
259 "ambiguous persistent cache backend: {count} backends compiled in, set `persistent_backend` in CacheConfig"
260 )]
261 AmbiguousCacheBackend { count: usize },
262
263 #[error("Checksum mismatch for {path}: expected {expected}, got {actual}")]
265 ChecksumMismatch {
266 path: PathBuf,
267 expected: String,
268 actual: String,
269 },
270
271 #[error("Invalid header '{header}': {reason}")]
273 InvalidHeader { header: String, reason: String },
274
275 #[error("Unexpected HTTP status {status} for {url}")]
277 UnexpectedStatus { status: u16, url: String },
278
279 #[error("Unexpected error: {0}")]
284 Unknown(String),
285}
286
287#[derive(Debug, Error)]
289pub enum ArchiveError {
290 #[error("ZIP extraction error: {0}")]
291 Zip(#[from] zip::result::ZipError),
292
293 #[error("Invalid archive format")]
294 InvalidFormat,
295
296 #[error("Corrupted archive")]
297 Corrupted,
298}
299
300impl Error {
303 pub fn io(operation: impl Into<String>, source: std::io::Error) -> Self {
314 let operation_str = operation.into();
315
316 tracing::warn!(
317 operation = operation_str,
318 error = %source,
319 "⚙️ IO error occurred"
320 );
321
322 Self::IO {
323 operation: operation_str,
324 path: None,
325 source,
326 }
327 }
328
329 pub fn io_with_path(operation: impl Into<String>, path: impl Into<PathBuf>, source: std::io::Error) -> Self {
341 let operation_str = operation.into();
342 let path_buf = path.into();
343
344 tracing::warn!(
345 operation = operation_str,
346 path = ?path_buf,
347 error = %source,
348 "⚙️ IO error occurred with path"
349 );
350
351 Self::IO {
352 operation: operation_str,
353 path: Some(path_buf),
354 source,
355 }
356 }
357
358 pub fn http(url: impl Into<String>, context: impl Into<String>, source: reqwest::Error) -> Self {
370 let url_str = url.into();
371 let context_str = context.into();
372
373 tracing::warn!(
374 url = url_str,
375 context = context_str,
376 error = %source,
377 is_timeout = source.is_timeout(),
378 is_connect = source.is_connect(),
379 status = ?source.status(),
380 "⚙️ HTTP error occurred"
381 );
382
383 Self::Http {
384 url: url_str,
385 context: context_str,
386 source,
387 }
388 }
389
390 pub fn json(context: impl Into<String>, source: serde_json::Error) -> Self {
401 let context_str = context.into();
402
403 tracing::warn!(
404 context = context_str,
405 error = %source,
406 line = source.line(),
407 column = source.column(),
408 "⚙️ JSON error occurred"
409 );
410
411 Self::Json {
412 context: context_str,
413 source,
414 }
415 }
416
417 #[cfg(feature = "cache-redb")]
428 pub fn database(operation: impl Into<String>, source: impl Into<redb::Error>) -> Self {
429 let operation_str = operation.into();
430 let source = source.into();
431
432 tracing::warn!(
433 operation = operation_str,
434 error = %source,
435 "⚙️ Database error occurred"
436 );
437
438 Self::Database {
439 operation: operation_str,
440 source: Box::new(source),
441 }
442 }
443
444 #[cfg(feature = "cache-redis")]
455 pub fn redis(operation: impl Into<String>, source: redis::RedisError) -> Self {
456 let operation_str = operation.into();
457
458 tracing::warn!(
459 operation = operation_str,
460 error = %source,
461 "⚙️ Redis error occurred"
462 );
463
464 Self::Redis {
465 operation: operation_str,
466 source,
467 }
468 }
469
470 pub fn runtime(context: impl Into<String>, source: tokio::task::JoinError) -> Self {
481 let context_str = context.into();
482
483 tracing::error!(
484 context = context_str,
485 error = %source,
486 is_cancelled = source.is_cancelled(),
487 is_panic = source.is_panic(),
488 "Runtime task error occurred"
489 );
490
491 Self::Runtime {
492 context: context_str,
493 source,
494 }
495 }
496
497 pub fn video_fetch(url: impl Into<String>, reason: impl Into<String>) -> Self {
508 let url_str = url.into();
509 let reason_str = reason.into();
510
511 tracing::warn!(url = url_str, reason = reason_str, "Video fetch failed");
512
513 Self::VideoFetch {
514 url: url_str,
515 reason: reason_str,
516 }
517 }
518
519 pub fn path_validation(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
530 let path_buf = path.into();
531 let reason_str = reason.into();
532
533 tracing::warn!(
534 path = ?path_buf,
535 reason = reason_str,
536 "⚙️ Path validation failed"
537 );
538
539 Self::PathValidation {
540 path: path_buf,
541 reason: reason_str,
542 }
543 }
544
545 pub fn url_validation(url: impl Into<String>, reason: impl Into<String>) -> Self {
556 let url_str = url.into();
557 let reason_str = reason.into();
558
559 tracing::warn!(url = url_str, reason = reason_str, "URL validation failed");
560
561 Self::UrlValidation {
562 url: url_str,
563 reason: reason_str,
564 }
565 }
566
567 pub fn invalid_partial_range(reason: impl Into<String>) -> Self {
577 let reason = reason.into();
578 tracing::warn!(reason = %reason, "Invalid partial range");
579 Self::InvalidPartialRange { reason }
580 }
581
582 pub fn download_failed(download_id: u64, reason: impl Into<String>) -> Self {
593 let reason_str = reason.into();
594
595 tracing::error!(download_id = download_id, reason = reason_str, "Download failed");
596
597 Self::DownloadFailed {
598 download_id,
599 reason: reason_str,
600 }
601 }
602
603 #[cfg(any(feature = "live-recording", feature = "live-streaming"))]
615 pub fn live_unavailable(url: impl Into<String>, live_status: impl Into<String>, reason: impl Into<String>) -> Self {
616 let url_str = url.into();
617 let live_status_str = live_status.into();
618 let reason_str = reason.into();
619
620 tracing::warn!(
621 url = url_str,
622 live_status = live_status_str,
623 reason = reason_str,
624 "📡 Live stream unavailable"
625 );
626
627 Self::LiveStreamUnavailable {
628 url: url_str,
629 live_status: live_status_str,
630 reason: reason_str,
631 }
632 }
633
634 #[cfg(any(feature = "live-recording", feature = "live-streaming"))]
645 pub fn hls_parsing(url: impl Into<String>, context: impl Into<String>) -> Self {
646 let url_str = url.into();
647 let context_str = context.into();
648
649 tracing::warn!(url = url_str, context = context_str, "HLS parsing failed");
650
651 Self::HlsParsing {
652 url: url_str,
653 context: context_str,
654 }
655 }
656
657 #[cfg(feature = "live-recording")]
668 pub fn live_recording(url: impl Into<String>, reason: impl Into<String>) -> Self {
669 let url_str = url.into();
670 let reason_str = reason.into();
671
672 tracing::error!(url = url_str, reason = reason_str, "Live recording failed");
673
674 Self::LiveRecording {
675 url: url_str,
676 reason: reason_str,
677 }
678 }
679
680 #[cfg(feature = "live-streaming")]
691 pub fn live_streaming(url: impl Into<String>, reason: impl Into<String>) -> Self {
692 let url_str = url.into();
693 let reason_str = reason.into();
694
695 tracing::error!(url = url_str, reason = reason_str, "Live streaming failed");
696
697 Self::LiveStreaming {
698 url: url_str,
699 reason: reason_str,
700 }
701 }
702
703 #[cfg(feature = "live-streaming")]
714 pub fn live_segment_fetch_failed(url: &str, status: reqwest::StatusCode) -> Self {
715 let reason = format!("{} {}", SEGMENT_FETCH_ERROR_PREFIX, status);
716 Self::live_streaming(url, reason)
717 }
718
719 pub fn metadata(operation: impl Into<String>, path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
731 let operation_str = operation.into();
732 let path_buf = path.into();
733 let reason_str = reason.into();
734
735 tracing::warn!(
736 operation = operation_str,
737 path = ?path_buf,
738 reason = reason_str,
739 "🏷️ Metadata operation failed"
740 );
741
742 Self::Metadata {
743 operation: operation_str,
744 path: path_buf,
745 reason: reason_str,
746 }
747 }
748
749 pub fn cache_miss(key: impl Into<String>) -> Self {
759 let key_str = key.into();
760 tracing::debug!(key = key_str, "🔍 Cache miss");
761 Self::CacheMiss { key: key_str }
762 }
763
764 pub fn cache_expired(key: impl Into<String>) -> Self {
774 let key_str = key.into();
775 tracing::debug!(key = key_str, "🔍 Cache entry expired");
776 Self::CacheExpired { key: key_str }
777 }
778
779 #[cfg(persistent_cache)]
789 pub fn ambiguous_cache_backend(count: usize) -> Self {
790 tracing::error!(count, "🔍 Ambiguous persistent cache backend");
791 Self::AmbiguousCacheBackend { count }
792 }
793}
794
795impl From<tokio::task::JoinError> for Error {
798 fn from(err: tokio::task::JoinError) -> Self {
799 tracing::error!(
800 error = %err,
801 is_cancelled = err.is_cancelled(),
802 is_panic = err.is_panic(),
803 "Task execution failed (automatic conversion)"
804 );
805
806 Self::Runtime {
807 context: "Task execution".to_string(),
808 source: err,
809 }
810 }
811}
812
813impl From<std::io::Error> for Error {
814 fn from(err: std::io::Error) -> Self {
815 tracing::warn!(
816 error = %err,
817 kind = ?err.kind(),
818 "⚙️ IO error (automatic conversion)"
819 );
820
821 Self::IO {
822 operation: "File operation".to_string(),
823 path: None,
824 source: err,
825 }
826 }
827}
828
829impl From<reqwest::Error> for Error {
830 fn from(err: reqwest::Error) -> Self {
831 let url = err.url().map(|u| u.to_string()).unwrap_or_default();
832
833 tracing::warn!(
834 url = url,
835 error = %err,
836 is_timeout = err.is_timeout(),
837 is_connect = err.is_connect(),
838 status = ?err.status(),
839 "⚙️ HTTP error (automatic conversion)"
840 );
841
842 Self::Http {
843 url,
844 context: "HTTP request".to_string(),
845 source: err,
846 }
847 }
848}
849
850impl From<serde_json::Error> for Error {
851 fn from(err: serde_json::Error) -> Self {
852 tracing::warn!(
853 error = %err,
854 line = err.line(),
855 column = err.column(),
856 "⚙️ JSON error (automatic conversion)"
857 );
858
859 Self::Json {
860 context: "JSON parsing".to_string(),
861 source: err,
862 }
863 }
864}
865
866#[cfg(feature = "cache-redb")]
867impl From<redb::Error> for Error {
868 fn from(err: redb::Error) -> Self {
869 tracing::warn!(
870 error = %err,
871 "⚙️ Database error (automatic conversion)"
872 );
873
874 Self::Database {
875 operation: "Database operation".to_string(),
876 source: Box::new(err),
877 }
878 }
879}
880
881#[cfg(feature = "cache-redis")]
882impl From<redis::RedisError> for Error {
883 fn from(err: redis::RedisError) -> Self {
884 tracing::warn!(
885 error = %err,
886 "⚙️ Redis error (automatic conversion)"
887 );
888
889 Self::Redis {
890 operation: "Redis operation".to_string(),
891 source: err,
892 }
893 }
894}
895
896impl From<media_seek::Error> for Error {
897 fn from(err: media_seek::Error) -> Self {
898 tracing::warn!(
899 error = %err,
900 "⚙️ media-seek error (automatic conversion)"
901 );
902
903 Self::InvalidPartialRange {
904 reason: err.to_string(),
905 }
906 }
907}
908
909impl From<zip::result::ZipError> for Error {
910 fn from(err: zip::result::ZipError) -> Self {
911 tracing::warn!(
912 error = %err,
913 "⚙️ ZIP archive error (automatic conversion)"
914 );
915
916 Self::Archive {
917 file: "unknown".to_string(),
918 source: ArchiveError::Zip(err),
919 }
920 }
921}
922#[cfg(feature = "live-streaming")]
924const SEGMENT_FETCH_ERROR_PREFIX: &str = "segment fetch returned HTTP";