Commit ac98c30
src/app.rs
@@ -114,7 +114,11 @@ impl App {
pub fn play_episode(&mut self) -> Result<()> {
if let Some(feed) = self.feeds.get(self.selected_feed) {
if let Some(episode) = feed.episodes.get(self.selected_episode) {
- self.player.play(&episode.enclosure_url)?;
+ // Play with caching - episodes get saved to ~/Music/{feed_name}/
+ self.player.play_with_cache(
+ &episode.enclosure_url,
+ Some((&feed.name, &episode.title))
+ )?;
// Set current track info and switch to NowPlaying screen
self.current_track = Some(CurrentTrack {
src/player.rs
@@ -1,6 +1,8 @@
use anyhow::{Context, Result};
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
+use std::fs;
use std::io::Cursor;
+use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
@@ -28,30 +30,34 @@ impl Player {
})
}
- pub fn play(&mut self, url: &str) -> Result<()> {
+ pub fn _play(&mut self, url: &str) -> Result<()> {
+ self.play_with_cache(url, None)
+ }
+
+ pub fn play_with_cache(&mut self, url: &str, cache_info: Option<(&str, &str)>) -> Result<()> {
// Stop any currently playing audio
self.stop();
+ let audio_data = if let Some((feed_name, episode_title)) = cache_info {
+ // Try to load from cache first
+ if let Ok(cached_data) = self.load_from_cache(feed_name, episode_title, url) {
+ cached_data
+ } else {
+ // Download and cache
+ self.download_and_cache(url, feed_name, episode_title)?
+ }
+ } else {
+ // Stream directly without caching
+ self.download_audio(url)?
+ };
-
- // Download the audio file
- let response = reqwest::blocking::get(url)
- .with_context(|| format!("Failed to fetch audio from URL: {}", url))?;
-
- let audio_data = response.bytes()
- .with_context(|| "Failed to read audio data")?;
-
-
-
- // Create a cursor from the downloaded data
+ // Create a cursor from the audio data
let cursor = Cursor::new(audio_data);
// Decode the audio
let source = Decoder::new(cursor)
.with_context(|| "Failed to decode audio file. Unsupported format?")?;
-
-
// Create a new sink
let sink = Sink::try_new(&self.stream_handle)
.with_context(|| "Failed to create audio sink")?;
@@ -67,10 +73,63 @@ impl Player {
self.start_time = Some(Instant::now());
self.is_paused = false;
-
Ok(())
}
+ fn download_audio(&self, url: &str) -> Result<Vec<u8>> {
+ let response = reqwest::blocking::get(url)
+ .with_context(|| format!("Failed to fetch audio from URL: {}", url))?;
+
+ let audio_data = response.bytes()
+ .with_context(|| "Failed to read audio data")?;
+
+ Ok(audio_data.to_vec())
+ }
+
+ fn cache_path(&self, feed_name: &str, _episode_title: &str, url: &str) -> PathBuf {
+ let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
+ let music_dir = home.join("Music").join(feed_name);
+
+ // Create filename from URL, handling query parameters
+ let filename = url.split('/').last().unwrap_or("episode.mp3");
+ let filename = filename.split('?').next().unwrap_or(filename);
+
+ // Sanitize filename
+ let safe_filename = filename.chars()
+ .map(|c| if c.is_alphanumeric() || c == '.' || c == '-' { c } else { '_' })
+ .collect::<String>();
+
+ music_dir.join(safe_filename)
+ }
+
+ fn load_from_cache(&self, feed_name: &str, episode_title: &str, url: &str) -> Result<Vec<u8>> {
+ let cache_path = self.cache_path(feed_name, episode_title, url);
+
+ if cache_path.exists() && cache_path.metadata()?.len() > 0 {
+ fs::read(&cache_path)
+ .with_context(|| format!("Failed to read cached file: {}", cache_path.display()))
+ } else {
+ Err(anyhow::anyhow!("No valid cache file found"))
+ }
+ }
+
+ fn download_and_cache(&self, url: &str, feed_name: &str, episode_title: &str) -> Result<Vec<u8>> {
+ let audio_data = self.download_audio(url)?;
+ let cache_path = self.cache_path(feed_name, episode_title, url);
+
+ // Create directory if it doesn't exist
+ if let Some(parent) = cache_path.parent() {
+ fs::create_dir_all(parent)
+ .with_context(|| format!("Failed to create cache directory: {}", parent.display()))?;
+ }
+
+ // Write to cache file
+ fs::write(&cache_path, &audio_data)
+ .with_context(|| format!("Failed to write cache file: {}", cache_path.display()))?;
+
+ Ok(audio_data)
+ }
+
pub fn stop(&mut self) {
if let Some(sink) = &self.sink {
if let Ok(sink) = sink.lock() {
@@ -111,7 +170,6 @@ impl Player {
// Note: rodio doesn't support seeking easily
// This is a limitation, but for now we'll just return Ok
// In the future, we could implement this with symphonia directly
-
Ok(())
}