Skip to main content

yt_dlp/
error.rs

1//! Error types with enhanced context and structured information.
2//!
3//! This module provides comprehensive error handling for the yt-dlp library,
4//! with detailed context, error chaining, and structured error information.
5
6use std::path::PathBuf;
7use std::time::Duration;
8
9use thiserror::Error;
10
11use crate::model::format::FormatType;
12use crate::utils::platform::{Architecture, Platform};
13
14/// A type alias for `Result<T, Error>`.
15pub type Result<T> = std::result::Result<T, Error>;
16
17/// The possible errors that can occur in the yt-dlp library.
18///
19/// Each error variant provides detailed context about what went wrong,
20/// including the operation being performed and any relevant parameters.
21#[derive(Debug, Error)]
22pub enum Error {
23    // ==================== Runtime & System Errors ====================
24    /// An async task failed to complete.
25    ///
26    /// This typically indicates a panic in a spawned tokio task or a cancellation.
27    #[error("Async task failed: {context}")]
28    Runtime {
29        context: String,
30        #[source]
31        source: tokio::task::JoinError,
32    },
33
34    /// A file system operation failed.
35    ///
36    /// Includes the operation being performed and the path involved.
37    #[error("IO error during {operation}")]
38    IO {
39        operation: String,
40        path: Option<PathBuf>,
41        #[source]
42        source: std::io::Error,
43    },
44
45    /// An archive extraction operation failed.
46    ///
47    /// This occurs when extracting yt-dlp or ffmpeg archives.
48    #[error("Failed to extract archive {file}: {source}")]
49    Archive {
50        file: String,
51        #[source]
52        source: ArchiveError,
53    },
54
55    // ==================== Network & HTTP Errors ====================
56    /// An HTTP request failed.
57    ///
58    /// Includes the URL being accessed and the operation context.
59    #[error("HTTP request failed for {url}: {context}")]
60    Http {
61        url: String,
62        context: String,
63        #[source]
64        source: reqwest::Error,
65    },
66
67    /// A network timeout occurred.
68    ///
69    /// Indicates the operation and duration that was exceeded.
70    #[error("Timeout after {duration:?} while {operation}")]
71    Timeout { operation: String, duration: Duration },
72
73    // ==================== Data & Serialization Errors ====================
74    /// JSON parsing or serialization failed.
75    ///
76    /// Includes the context of what was being parsed/serialized.
77    #[error("JSON error while {context}: {source}")]
78    Json {
79        context: String,
80        #[source]
81        source: serde_json::Error,
82    },
83
84    /// Database operation failed (redb backend).
85    ///
86    /// Includes the specific operation that failed.
87    #[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    /// Redis operation failed.
96    ///
97    /// Includes the specific operation that failed.
98    #[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    // ==================== Dependency & Binary Errors ====================
107    /// No GitHub release asset found for the current platform.
108    ///
109    /// This occurs when trying to download yt-dlp or ffmpeg binaries.
110    #[error("No {binary} release found for {platform}/{architecture}")]
111    NoBinaryRelease {
112        binary: String,
113        platform: Platform,
114        architecture: Architecture,
115    },
116
117    /// The required binary was not found after installation.
118    ///
119    /// This indicates an installation issue or corrupted download.
120    #[error("{binary} binary not found at {path} after installation")]
121    BinaryNotFound { binary: String, path: PathBuf },
122
123    /// Command execution failed.
124    ///
125    /// Includes the command, exit status, and stderr output.
126    #[error("Command '{command}' failed (exit code: {exit_code}): {stderr}")]
127    CommandFailed {
128        command: String,
129        exit_code: i32,
130        stderr: String,
131    },
132
133    // ==================== Video & Format Errors ====================
134    /// Failed to fetch video information from YouTube.
135    ///
136    /// Includes the URL and reason for failure.
137    #[error("Failed to fetch video from {url}: {reason}")]
138    VideoFetch { url: String, reason: String },
139
140    /// Video information is missing expected data.
141    ///
142    /// This occurs when YouTube's API returns incomplete data.
143    #[error("Video {video_id} is missing required field: {field}")]
144    VideoMissingField { video_id: String, field: String },
145
146    /// The requested format is not available.
147    ///
148    /// Includes the format type and available alternatives.
149    #[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    /// The format has no URL available for download.
157    ///
158    /// This can occur with DRM-protected or geo-restricted content.
159    #[error("Format {format_id} for video {video_id} has no download URL")]
160    FormatNoUrl { video_id: String, format_id: String },
161
162    /// The format is incompatible with the requested operation.
163    ///
164    /// For example, trying to extract audio from a video-only format.
165    #[error("Format {format_id} is incompatible: {reason}")]
166    FormatIncompatible { format_id: String, reason: String },
167
168    /// No thumbnail is available for the video.
169    #[error("No thumbnail available for video {video_id}")]
170    NoThumbnail { video_id: String },
171
172    /// No subtitles are available for the requested language.
173    #[error("No subtitles available for video {video_id} in language '{language}'")]
174    SubtitleNotAvailable { video_id: String, language: String },
175
176    /// The URL has expired and needs to be refreshed.
177    #[error("URL expired")]
178    UrlExpired,
179
180    // ==================== Path & Security Errors ====================
181    /// Path validation failed due to security concerns.
182    ///
183    /// This prevents path traversal and other security issues.
184    #[error("Invalid path '{path}': {reason}")]
185    PathValidation { path: PathBuf, reason: String },
186
187    /// URL validation failed.
188    ///
189    /// This ensures only valid YouTube URLs are processed.
190    #[error("Invalid URL '{url}': {reason}")]
191    UrlValidation { url: String, reason: String },
192
193    // ==================== Download Errors ====================
194    /// Download operation failed.
195    ///
196    /// Includes the download ID and reason for failure.
197    #[error("Download {download_id} failed: {reason}")]
198    DownloadFailed { download_id: u64, reason: String },
199
200    /// Download was cancelled by user or system.
201    #[error("Download {download_id} was cancelled")]
202    DownloadCancelled { download_id: u64 },
203
204    /// A partial download range is invalid or the container format does not support seeking.
205    #[error("Invalid partial range: {reason}")]
206    InvalidPartialRange { reason: String },
207
208    // ==================== Live Stream Errors ====================
209    /// The video is not currently a live stream.
210    #[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    /// Failed to parse an HLS manifest.
219    #[cfg(any(feature = "live-recording", feature = "live-streaming"))]
220    #[error("HLS parsing failed for {url}: {context}")]
221    HlsParsing { url: String, context: String },
222
223    /// A live recording operation failed.
224    #[cfg(feature = "live-recording")]
225    #[error("Live recording failed for {url}: {reason}")]
226    LiveRecording { url: String, reason: String },
227
228    /// A live streaming operation failed.
229    #[cfg(feature = "live-streaming")]
230    #[error("Live streaming failed for {url}: {reason}")]
231    LiveStreaming { url: String, reason: String },
232
233    // ==================== Metadata Errors ====================
234    /// A metadata tagging operation failed.
235    ///
236    /// This occurs when reading or writing audio/video tags (ID3, MP4, lofty).
237    #[error("Metadata {operation} failed for {path}: {reason}")]
238    Metadata {
239        operation: String,
240        path: PathBuf,
241        reason: String,
242    },
243
244    // ==================== Cache Errors ====================
245    /// The requested item was not found in the cache.
246    #[error("Cache miss for {key}")]
247    CacheMiss { key: String },
248
249    /// The cached item has expired.
250    #[error("Cache entry expired for {key}")]
251    CacheExpired { key: String },
252
253    /// Multiple persistent cache backends are compiled in but none was selected.
254    ///
255    /// Set `CacheConfig::persistent_backend` explicitly when more than one of
256    /// `cache-json`, `cache-redb`, or `cache-redis` features are active.
257    #[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    /// A checksum verification failed after downloading.
264    #[error("Checksum mismatch for {path}: expected {expected}, got {actual}")]
265    ChecksumMismatch {
266        path: PathBuf,
267        expected: String,
268        actual: String,
269    },
270
271    /// An HTTP header value was invalid.
272    #[error("Invalid header '{header}': {reason}")]
273    InvalidHeader { header: String, reason: String },
274
275    /// An HTTP response status was unexpected.
276    #[error("Unexpected HTTP status {status} for {url}")]
277    UnexpectedStatus { status: u16, url: String },
278
279    // ==================== Generic Errors ====================
280    /// An unexpected error occurred that doesn't fit other categories.
281    ///
282    /// This should be used sparingly and ideally replaced with more specific variants.
283    #[error("Unexpected error: {0}")]
284    Unknown(String),
285}
286
287/// Archive extraction errors.
288#[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
300// ==================== Helper constructors for common error patterns ====================
301
302impl Error {
303    /// Create an IO error with operation context.
304    ///
305    /// # Arguments
306    ///
307    /// * `operation` - Description of the operation that failed
308    /// * `source` - The underlying IO error
309    ///
310    /// # Returns
311    ///
312    /// An Error::IO variant with the provided context
313    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    /// Create an IO error with operation and path context.
330    ///
331    /// # Arguments
332    ///
333    /// * `operation` - Description of the operation that failed
334    /// * `path` - The file path involved in the operation
335    /// * `source` - The underlying IO error
336    ///
337    /// # Returns
338    ///
339    /// An Error::IO variant with the provided context and path
340    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    /// Create an HTTP error with URL context.
359    ///
360    /// # Arguments
361    ///
362    /// * `url` - The URL that was being accessed
363    /// * `context` - Additional context about the operation
364    /// * `source` - The underlying reqwest error
365    ///
366    /// # Returns
367    ///
368    /// An Error::Http variant with the provided context
369    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    /// Create a JSON parsing error with context.
391    ///
392    /// # Arguments
393    ///
394    /// * `context` - Description of what was being parsed/serialized
395    /// * `source` - The underlying serde_json error
396    ///
397    /// # Returns
398    ///
399    /// An Error::Json variant with the provided context
400    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    /// Create a database error with operation context (redb).
418    ///
419    /// # Arguments
420    ///
421    /// * `operation` - Description of the database operation that failed
422    /// * `source` - The underlying redb error
423    ///
424    /// # Returns
425    ///
426    /// An Error::Database variant with the provided context
427    #[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    /// Create a Redis error with operation context.
445    ///
446    /// # Arguments
447    ///
448    /// * `operation` - Description of the Redis operation that failed
449    /// * `source` - The underlying Redis error
450    ///
451    /// # Returns
452    ///
453    /// An Error::Redis variant with the provided context
454    #[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    /// Create a runtime error with context.
471    ///
472    /// # Arguments
473    ///
474    /// * `context` - Description of the task that failed
475    /// * `source` - The underlying tokio JoinError
476    ///
477    /// # Returns
478    ///
479    /// An Error::Runtime variant with the provided context
480    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    /// Create a video fetch error.
498    ///
499    /// # Arguments
500    ///
501    /// * `url` - The URL that failed to fetch
502    /// * `reason` - The reason for the fetch failure
503    ///
504    /// # Returns
505    ///
506    /// An Error::VideoFetch variant with the provided details
507    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    /// Create a path validation error.
520    ///
521    /// # Arguments
522    ///
523    /// * `path` - The path that failed validation
524    /// * `reason` - The reason for validation failure
525    ///
526    /// # Returns
527    ///
528    /// An Error::PathValidation variant with the provided details
529    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    /// Create a URL validation error.
546    ///
547    /// # Arguments
548    ///
549    /// * `url` - The URL that failed validation
550    /// * `reason` - The reason for validation failure
551    ///
552    /// # Returns
553    ///
554    /// An Error::UrlValidation variant with the provided details
555    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    /// Create an invalid partial range error.
568    ///
569    /// # Arguments
570    ///
571    /// * `reason` - Description of why the partial range is invalid
572    ///
573    /// # Returns
574    ///
575    /// An Error::InvalidPartialRange variant with the provided reason
576    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    /// Create a download failed error.
583    ///
584    /// # Arguments
585    ///
586    /// * `download_id` - The ID of the download that failed
587    /// * `reason` - The reason for the download failure
588    ///
589    /// # Returns
590    ///
591    /// An Error::DownloadFailed variant with the provided details
592    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    /// Create a live stream unavailable error.
604    ///
605    /// # Arguments
606    ///
607    /// * `url` - The URL of the video
608    /// * `live_status` - The current live status of the video
609    /// * `reason` - Why the stream is not available
610    ///
611    /// # Returns
612    ///
613    /// An Error::LiveStreamUnavailable variant with the provided details
614    #[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    /// Create an HLS parsing error.
635    ///
636    /// # Arguments
637    ///
638    /// * `url` - The URL of the manifest that failed to parse
639    /// * `context` - Description of the parsing failure
640    ///
641    /// # Returns
642    ///
643    /// An Error::HlsParsing variant with the provided details
644    #[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    /// Create a live recording error.
658    ///
659    /// # Arguments
660    ///
661    /// * `url` - The URL of the live stream
662    /// * `reason` - Why the recording failed
663    ///
664    /// # Returns
665    ///
666    /// An Error::LiveRecording variant with the provided details
667    #[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    /// Create a live streaming error.
681    ///
682    /// # Arguments
683    ///
684    /// * `url` - The URL of the live stream
685    /// * `reason` - Why the streaming failed
686    ///
687    /// # Returns
688    ///
689    /// An Error::LiveStreaming variant with the provided details
690    #[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    /// Create an error for a failed live segment fetch.
704    ///
705    /// # Arguments
706    ///
707    /// * `url` - The URL of the live segment
708    /// * `status` - The HTTP status code returned
709    ///
710    /// # Returns
711    ///
712    /// An Error::LiveStreaming variant with the provided details
713    #[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    /// Create a metadata error with operation and path context.
720    ///
721    /// # Arguments
722    ///
723    /// * `operation` - Description of the metadata operation (e.g. "read MP4 tags")
724    /// * `path` - The file path involved
725    /// * `reason` - The reason for the failure
726    ///
727    /// # Returns
728    ///
729    /// An Error::Metadata variant with the provided context
730    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    /// Create a cache miss error.
750    ///
751    /// # Arguments
752    ///
753    /// * `key` - The cache key that was not found
754    ///
755    /// # Returns
756    ///
757    /// An Error::CacheMiss variant
758    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    /// Create a cache expired error.
765    ///
766    /// # Arguments
767    ///
768    /// * `key` - The cache key that expired
769    ///
770    /// # Returns
771    ///
772    /// An Error::CacheExpired variant
773    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    /// Create an ambiguous cache backend error.
780    ///
781    /// # Arguments
782    ///
783    /// * `count` - The number of persistent backends compiled in
784    ///
785    /// # Returns
786    ///
787    /// An Error::AmbiguousCacheBackend variant
788    #[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
795// ==================== Automatic conversions for convenience ====================
796
797impl 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/// Error context prefix for failed segment fetches.
923#[cfg(feature = "live-streaming")]
924const SEGMENT_FETCH_ERROR_PREFIX: &str = "segment fetch returned HTTP";