main
  1use anyhow::{Context, Result};
  2use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink, Source};
  3use std::fs;
  4use std::io::Cursor;
  5use std::path::PathBuf;
  6use std::sync::{Arc, Mutex};
  7use std::time::Instant;
  8
  9pub struct Player {
 10    _stream: OutputStream,
 11    stream_handle: OutputStreamHandle,
 12    sink: Option<Arc<Mutex<Sink>>>,
 13    start_time: Option<Instant>,
 14    pause_time: Option<Instant>,
 15    paused_duration: std::time::Duration,
 16    seek_position: f64, // Seconds
 17    is_paused: bool,
 18    volume: f32,
 19    current_source: Option<Vec<u8>>, // Store audio data for seeking
 20    current_cache_info: Option<(String, String)>, // For re-loading
 21    current_url: Option<String>,
 22}
 23
 24impl Player {
 25    pub fn new() -> Result<Self> {
 26        let (stream, stream_handle) =
 27            OutputStream::try_default().with_context(|| "Failed to create audio output stream")?;
 28
 29        Ok(Self {
 30            _stream: stream,
 31            stream_handle,
 32            sink: None,
 33            start_time: None,
 34            pause_time: None,
 35            paused_duration: std::time::Duration::ZERO,
 36            seek_position: 0.0,
 37            is_paused: false,
 38            volume: 0.7, // 70% volume
 39            current_source: None,
 40            current_cache_info: None,
 41            current_url: None,
 42        })
 43    }
 44
 45    pub fn _play(&mut self, url: &str) -> Result<()> {
 46        self.play_with_cache(url, None)
 47    }
 48
 49    pub fn play_with_cache(&mut self, url: &str, cache_info: Option<(&str, &str)>) -> Result<()> {
 50        self.play_from_source(url, cache_info, false)
 51    }
 52
 53    pub fn play_from_local_file(&mut self, file_path: &str) -> Result<()> {
 54        self.play_from_source(file_path, None, true)
 55    }
 56
 57    fn play_from_source(
 58        &mut self,
 59        source: &str,
 60        cache_info: Option<(&str, &str)>,
 61        is_local_file: bool,
 62    ) -> Result<()> {
 63        // Stop any currently playing audio
 64        self.stop();
 65
 66        let audio_data = if is_local_file {
 67            // Read directly from local file
 68            fs::read(source)
 69                .with_context(|| format!("Failed to read local audio file: {}", source))?
 70        } else if let Some((feed_name, episode_title)) = cache_info {
 71            // Try to load from cache first
 72            if let Ok(cached_data) = self.load_from_cache(feed_name, episode_title, source) {
 73                cached_data
 74            } else {
 75                // Download and cache
 76                self.download_and_cache(source, feed_name, episode_title)?
 77            }
 78        } else {
 79            // Stream directly without caching
 80            self.download_audio(source)?
 81        };
 82
 83        // Store audio data and metadata for seeking
 84        self.current_source = Some(audio_data.clone());
 85        self.current_cache_info = cache_info.map(|(f, e)| (f.to_string(), e.to_string()));
 86        self.current_url = Some(source.to_string());
 87        self.seek_position = 0.0;
 88        self.paused_duration = std::time::Duration::ZERO;
 89
 90        // Start playback from the beginning
 91        self.start_playback_from_position(&audio_data, 0.0)?;
 92
 93        Ok(())
 94    }
 95
 96    fn start_playback_from_position(
 97        &mut self,
 98        audio_data: &[u8],
 99        position_seconds: f64,
100    ) -> Result<()> {
101        // Create a cursor from the audio data
102        let cursor = Cursor::new(audio_data.to_vec());
103
104        // Decode the audio
105        let mut decoded_source = Decoder::new(cursor)
106            .with_context(|| "Failed to decode audio file. Unsupported format?")?;
107
108        // Skip to the desired position (approximate)
109        if position_seconds > 0.0 {
110            let sample_rate = decoded_source.sample_rate() as f64;
111            let channels = decoded_source.channels() as f64;
112            let samples_to_skip = (position_seconds * sample_rate * channels) as usize;
113
114            // Skip samples (this is approximate but works for seeking)
115            for _ in 0..samples_to_skip {
116                if decoded_source.next().is_none() {
117                    break;
118                }
119            }
120        }
121
122        // Create a new sink
123        let sink =
124            Sink::try_new(&self.stream_handle).with_context(|| "Failed to create audio sink")?;
125
126        // Set volume
127        sink.set_volume(self.volume);
128
129        // Add the source to the sink
130        sink.append(decoded_source);
131
132        // Store the sink and adjust timing
133        self.sink = Some(Arc::new(Mutex::new(sink)));
134        self.start_time = Some(Instant::now());
135        self.seek_position = position_seconds;
136        self.is_paused = false;
137        self.pause_time = None;
138
139        Ok(())
140    }
141
142    fn download_audio(&self, url: &str) -> Result<Vec<u8>> {
143        let response = reqwest::blocking::get(url)
144            .with_context(|| format!("Failed to fetch audio from URL: {}", url))?;
145
146        let audio_data = response
147            .bytes()
148            .with_context(|| "Failed to read audio data")?;
149
150        Ok(audio_data.to_vec())
151    }
152
153    fn cache_path(&self, feed_name: &str, _episode_title: &str, url: &str) -> PathBuf {
154        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
155        let music_dir = home.join("Music").join(feed_name);
156
157        // Create filename from URL, handling query parameters
158        let filename = url.split('/').last().unwrap_or("episode.mp3");
159        let filename = filename.split('?').next().unwrap_or(filename);
160
161        // Sanitize filename
162        let safe_filename = filename
163            .chars()
164            .map(|c| {
165                if c.is_alphanumeric() || c == '.' || c == '-' {
166                    c
167                } else {
168                    '_'
169                }
170            })
171            .collect::<String>();
172
173        music_dir.join(safe_filename)
174    }
175
176    fn load_from_cache(&self, feed_name: &str, episode_title: &str, url: &str) -> Result<Vec<u8>> {
177        let cache_path = self.cache_path(feed_name, episode_title, url);
178
179        if cache_path.exists() && cache_path.metadata()?.len() > 0 {
180            fs::read(&cache_path)
181                .with_context(|| format!("Failed to read cached file: {}", cache_path.display()))
182        } else {
183            Err(anyhow::anyhow!("No valid cache file found"))
184        }
185    }
186
187    fn download_and_cache(
188        &self,
189        url: &str,
190        feed_name: &str,
191        episode_title: &str,
192    ) -> Result<Vec<u8>> {
193        let audio_data = self.download_audio(url)?;
194        let cache_path = self.cache_path(feed_name, episode_title, url);
195
196        // Create directory if it doesn't exist
197        if let Some(parent) = cache_path.parent() {
198            fs::create_dir_all(parent).with_context(|| {
199                format!("Failed to create cache directory: {}", parent.display())
200            })?;
201        }
202
203        // Write to cache file
204        fs::write(&cache_path, &audio_data)
205            .with_context(|| format!("Failed to write cache file: {}", cache_path.display()))?;
206
207        Ok(audio_data)
208    }
209
210    pub fn stop(&mut self) {
211        if let Some(sink) = &self.sink {
212            if let Ok(sink) = sink.lock() {
213                sink.stop();
214            }
215        }
216        self.sink = None;
217        self.start_time = None;
218        self.pause_time = None;
219        self.paused_duration = std::time::Duration::ZERO;
220        self.seek_position = 0.0;
221        self.is_paused = false;
222        // Keep current_source, current_cache_info, current_url for potential resume
223    }
224
225    pub fn toggle_pause(&mut self) -> Result<()> {
226        if let Some(sink) = &self.sink {
227            if let Ok(sink) = sink.lock() {
228                if self.is_paused {
229                    // Resume: adjust start time to account for pause duration
230                    if let Some(pause_time) = self.pause_time {
231                        self.paused_duration += pause_time.elapsed();
232                    }
233                    sink.play();
234                    self.is_paused = false;
235                    self.pause_time = None;
236                } else {
237                    // Pause: record when we paused
238                    sink.pause();
239                    self.is_paused = true;
240                    self.pause_time = Some(Instant::now());
241                }
242            }
243        }
244        Ok(())
245    }
246
247    pub fn set_volume(&mut self, volume: u8) -> Result<()> {
248        self.volume = (volume as f32) / 100.0;
249        if let Some(sink) = &self.sink {
250            if let Ok(sink) = sink.lock() {
251                sink.set_volume(self.volume);
252            }
253        }
254        Ok(())
255    }
256
257    pub fn seek(&mut self, seconds: f64) -> Result<()> {
258        // Calculate new position
259        let current_pos = self.get_position();
260        let new_position = (current_pos + seconds).max(0.0);
261
262        // Seek to absolute position
263        self.seek_to(new_position)
264    }
265
266    pub fn seek_to(&mut self, position_seconds: f64) -> Result<()> {
267        // Can only seek if we have audio data loaded
268        if let Some(audio_data) = &self.current_source {
269            let audio_data = audio_data.clone();
270            let was_paused = self.is_paused;
271
272            // Stop current playback
273            if let Some(sink) = &self.sink {
274                if let Ok(sink) = sink.lock() {
275                    sink.stop();
276                }
277            }
278
279            // Start playback from new position
280            self.start_playback_from_position(&audio_data, position_seconds)?;
281
282            // If we were paused, pause again
283            if was_paused {
284                self.toggle_pause()?;
285            }
286        }
287
288        Ok(())
289    }
290
291    // Jump forward by a specific amount (e.g., 15 seconds)
292    pub fn skip_forward(&mut self, seconds: f64) -> Result<()> {
293        self.seek(seconds)
294    }
295
296    // Jump backward by a specific amount (e.g., 15 seconds)
297    pub fn skip_backward(&mut self, seconds: f64) -> Result<()> {
298        self.seek(-seconds)
299    }
300
301    pub fn is_paused(&self) -> bool {
302        self.is_paused
303    }
304
305    pub fn get_position(&self) -> f64 {
306        if let Some(start_time) = self.start_time {
307            let elapsed = if self.is_paused {
308                // If paused, calculate time up to when we paused
309                if let Some(pause_time) = self.pause_time {
310                    start_time.elapsed() - pause_time.elapsed()
311                } else {
312                    start_time.elapsed()
313                }
314            } else {
315                // If playing, subtract total paused duration
316                start_time.elapsed() - self.paused_duration
317            };
318
319            // Add the seek position offset
320            self.seek_position + elapsed.as_secs_f64()
321        } else {
322            self.seek_position
323        }
324    }
325
326    // Get duration if available (this would require parsing the audio file)
327    pub fn get_duration(&self) -> Option<f64> {
328        // For now, return None - we could implement this by pre-parsing the audio file
329        // or storing duration from RSS feed metadata
330        None
331    }
332
333    // Get position as formatted time string
334    pub fn get_position_formatted(&self) -> String {
335        format_duration(self.get_position())
336    }
337
338    // Get duration as formatted time string
339    pub fn get_duration_formatted(&self) -> String {
340        if let Some(duration) = self.get_duration() {
341            format_duration(duration)
342        } else {
343            "--:--".to_string()
344        }
345    }
346
347    pub fn _is_playing(&self) -> bool {
348        if let Some(sink) = &self.sink {
349            if let Ok(sink) = sink.lock() {
350                !sink.empty() && !self.is_paused
351            } else {
352                false
353            }
354        } else {
355            false
356        }
357    }
358}
359
360impl Drop for Player {
361    fn drop(&mut self) {
362        self.stop();
363    }
364}
365
366fn format_duration(seconds: f64) -> String {
367    let total_seconds = seconds as u64;
368    let hours = total_seconds / 3600;
369    let minutes = (total_seconds % 3600) / 60;
370    let secs = total_seconds % 60;
371
372    if hours > 0 {
373        format!("{}:{:02}:{:02}", hours, minutes, secs)
374    } else {
375        format!("{}:{:02}", minutes, secs)
376    }
377}