audio_engine_core/decoder/
error.rs1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3#[cfg(feature = "http")]
4use std::time::Duration;
5
6use thiserror::Error;
7
8#[cfg(feature = "http")]
9const NETWORK_MAX_ATTEMPTS: usize = 3;
10#[cfg(feature = "http")]
11const NETWORK_BACKOFF_DELAYS: [Duration; 2] = [Duration::from_secs(1), Duration::from_secs(2)];
12
13#[cfg(feature = "http")]
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum NetworkError {
20 HttpTimeout,
22 ConnectionReset,
24 HttpStatus(u16),
26 DnsFailure(String),
28 TlsError(String),
30 Other(String),
32}
33
34#[cfg(feature = "http")]
35impl NetworkError {
36 pub fn is_retriable(&self) -> bool {
39 match self {
40 NetworkError::HttpTimeout | NetworkError::ConnectionReset => true,
41 NetworkError::HttpStatus(status) => matches!(status, 408 | 429 | 500..=504),
42 NetworkError::DnsFailure(_) | NetworkError::TlsError(_) | NetworkError::Other(_) => {
43 false
44 }
45 }
46 }
47
48 pub(super) fn from_io(e: std::io::Error) -> Self {
49 match e.kind() {
50 std::io::ErrorKind::TimedOut => NetworkError::HttpTimeout,
51 std::io::ErrorKind::ConnectionReset => NetworkError::ConnectionReset,
52 _ => NetworkError::Other(e.to_string()),
53 }
54 }
55
56 fn is_decode_cancelled(&self) -> bool {
57 matches!(self, NetworkError::Other(message) if message == "Decode cancelled")
58 }
59}
60
61#[cfg(feature = "http")]
62pub(super) fn network_error_to_decoder_error(error: NetworkError) -> DecoderError {
63 if error.is_decode_cancelled() {
64 DecoderError::Canceled
65 } else {
66 DecoderError::Network(error)
67 }
68}
69
70#[cfg(feature = "http")]
71impl From<reqwest::Error> for NetworkError {
72 fn from(e: reqwest::Error) -> Self {
73 if e.is_timeout() {
74 NetworkError::HttpTimeout
75 } else if let Some(status) = e.status() {
76 NetworkError::HttpStatus(status.as_u16())
77 } else {
78 let text = e.to_string();
79 let lower = text.to_ascii_lowercase();
80 if lower.contains("connection reset") {
81 NetworkError::ConnectionReset
82 } else if e.is_connect() && (lower.contains("dns") || lower.contains("resolve")) {
83 NetworkError::DnsFailure(text)
84 } else if lower.contains("tls") || lower.contains("certificate") {
85 NetworkError::TlsError(text)
86 } else {
87 NetworkError::Other(text)
88 }
89 }
90 }
91}
92
93#[cfg(feature = "http")]
94impl std::fmt::Display for NetworkError {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 NetworkError::HttpTimeout => write!(f, "HTTP timeout"),
98 NetworkError::ConnectionReset => write!(f, "connection reset"),
99 NetworkError::HttpStatus(status) => write!(f, "HTTP status {}", status),
100 NetworkError::DnsFailure(e) => write!(f, "DNS failure: {}", e),
101 NetworkError::TlsError(e) => write!(f, "TLS error: {}", e),
102 NetworkError::Other(e) => write!(f, "{}", e),
103 }
104 }
105}
106
107#[derive(Clone, Debug)]
109pub struct DecodeCancelToken {
110 cancelled: Arc<AtomicBool>,
111}
112
113impl DecodeCancelToken {
114 pub fn new(cancelled: Arc<AtomicBool>) -> Self {
117 Self { cancelled }
118 }
119
120 pub fn is_cancelled(&self) -> bool {
123 self.cancelled.load(Ordering::Acquire)
124 }
125}
126
127#[derive(Error, Debug)]
129pub enum DecoderError {
130 #[error("Failed to open file: {0}")]
132 FileOpen(#[from] std::io::Error),
133 #[cfg(feature = "http")]
135 #[error("Network error: {0}")]
136 Network(NetworkError),
137 #[error("Unsupported format")]
139 UnsupportedFormat,
140 #[error("No audio track found")]
142 NoAudioTrack,
143 #[error("Decoder error: {0}")]
145 Decoder(String),
146 #[error("Probe error: {0}")]
148 Probe(String),
149 #[error("Decode cancelled")]
151 Canceled,
152}
153
154#[cfg(feature = "http")]
155#[allow(clippy::needless_range_loop)]
157pub(super) fn with_network_retry<T, F>(operation_name: &str, mut op: F) -> Result<T, NetworkError>
158where
159 F: FnMut() -> Result<T, NetworkError>,
160{
161 for attempt in 0..NETWORK_MAX_ATTEMPTS {
162 match op() {
163 Ok(value) => return Ok(value),
164 Err(e) if e.is_retriable() && attempt < NETWORK_BACKOFF_DELAYS.len() => {
165 let delay = NETWORK_BACKOFF_DELAYS[attempt];
166 log::warn!(
167 "{} attempt {} failed ({}), retrying in {:?}",
168 operation_name,
169 attempt + 1,
170 e,
171 delay
172 );
173 std::thread::sleep(delay);
174 }
175 Err(e) => return Err(e),
176 }
177 }
178
179 unreachable!("network retry loop returns on success or final error")
180}