Commit c442980
src/app.rs
@@ -1,5 +1,7 @@
use anyhow::Result;
use crate::{config::Config, feed::Feed, player::Player};
+use std::sync::mpsc;
+use std::thread;
#[derive(Debug, Clone, Copy)]
pub enum CurrentScreen {
@@ -17,6 +19,7 @@ pub struct App {
pub player: Player,
pub current_track: Option<CurrentTrack>,
pub volume: u8,
+ feed_receiver: mpsc::Receiver<(usize, Feed)>,
}
#[derive(Debug, Clone)]
@@ -30,16 +33,25 @@ pub struct CurrentTrack {
impl App {
pub fn new() -> Result<Self> {
let config = Config::load()?;
- let mut feeds: Vec<Feed> = config.feeds
+ let feeds: Vec<Feed> = config.feeds
.iter()
.map(|(name, url)| Feed::new(name.clone(), url.clone()))
.collect();
- // Auto-load episodes on startup
- for feed in &mut feeds {
- if let Err(e) = feed.fetch_episodes() {
- eprintln!("Failed to fetch episodes for {}: {}", feed.name, e);
- }
+ // Set up async feed loading
+ let (sender, receiver) = mpsc::channel();
+
+ // Start background feed loading
+ for (index, (name, url)) in config.feeds.iter().enumerate() {
+ let sender = sender.clone();
+ let name = name.clone();
+ let url = url.clone();
+
+ thread::spawn(move || {
+ let mut feed = Feed::new(name, url);
+ let _ = feed.fetch_episodes(); // Ignore errors for now, state tracks them
+ let _ = sender.send((index, feed));
+ });
}
Ok(Self {
@@ -51,6 +63,7 @@ impl App {
player: Player::new()?,
current_track: None,
volume: 70, // Default volume 70%
+ feed_receiver: receiver,
})
}
@@ -173,4 +186,14 @@ impl App {
pub fn back_to_episodes(&mut self) {
self.current_screen = CurrentScreen::EpisodeList;
}
+
+ // Check for and process incoming feed updates
+ pub fn update_feeds(&mut self) {
+ // Process all available feed updates without blocking
+ while let Ok((index, feed)) = self.feed_receiver.try_recv() {
+ if index < self.feeds.len() {
+ self.feeds[index] = feed;
+ }
+ }
+ }
}
\ No newline at end of file
src/feed.rs
@@ -3,11 +3,19 @@ use chrono::{DateTime, Utc};
use quick_xml::de::from_str;
use serde::Deserialize;
+#[derive(Debug, Clone)]
+pub enum FeedState {
+ Loading,
+ Loaded,
+ Error(String),
+}
+
#[derive(Debug, Clone)]
pub struct Feed {
pub name: String,
pub url: String,
pub episodes: Vec<Episode>,
+ pub state: FeedState,
}
#[derive(Debug, Clone)]
@@ -53,10 +61,26 @@ impl Feed {
name,
url,
episodes: Vec::new(),
+ state: FeedState::Loading,
}
}
pub fn fetch_episodes(&mut self) -> Result<()> {
+ self.state = FeedState::Loading;
+
+ match self.try_fetch_episodes() {
+ Ok(()) => {
+ self.state = FeedState::Loaded;
+ Ok(())
+ }
+ Err(e) => {
+ self.state = FeedState::Error(e.to_string());
+ Err(e)
+ }
+ }
+ }
+
+ fn try_fetch_episodes(&mut self) -> Result<()> {
let response = reqwest::blocking::get(&self.url)
.with_context(|| format!("Failed to fetch feed: {}", self.url))?;
src/main.rs
@@ -96,6 +96,9 @@ fn main() -> Result<()> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
loop {
+ // Update feeds from background loading
+ app.update_feeds();
+
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
@@ -142,7 +145,14 @@ fn ui(f: &mut Frame, app: &App) {
let feeds: Vec<ListItem> = app
.feeds
.iter()
- .map(|feed| ListItem::new(Line::from(feed.name.clone())))
+ .map(|feed| {
+ let display_name = match &feed.state {
+ crate::feed::FeedState::Loading => format!("ā³ {}", feed.name),
+ crate::feed::FeedState::Loaded => format!("ā {} ({})", feed.name, feed.episodes.len()),
+ crate::feed::FeedState::Error(_) => format!("ā {}", feed.name),
+ };
+ ListItem::new(Line::from(display_name))
+ })
.collect();
let feeds_list = List::new(feeds)
@@ -162,12 +172,35 @@ fn ui(f: &mut Frame, app: &App) {
// Right panel - Episodes or info
match app.current_screen {
CurrentScreen::FeedList => {
+ let loading_count = app.feeds.iter().filter(|f| matches!(f.state, crate::feed::FeedState::Loading)).count();
+ let loaded_count = app.feeds.iter().filter(|f| matches!(f.state, crate::feed::FeedState::Loaded)).count();
+ let error_count = app.feeds.iter().filter(|f| matches!(f.state, crate::feed::FeedState::Error(_))).count();
+
let info_text = if app.feeds.is_empty() {
- "No feeds configured!\n\nEdit ~/.config/ghetto-blaster.yml\nto add podcast feeds.\n\nThen press 'r' to refresh."
- } else if app.feeds.iter().all(|f| f.episodes.is_empty()) {
- "Loading episodes...\n\nIf this persists:\n⢠Check your internet connection\n⢠Verify feed URLs are valid\n⢠Press 'r' to refresh"
+ "No feeds configured!
+
+Edit ~/.config/ghetto-blaster.yml
+to add podcast feeds.
+
+Then press 'r' to refresh.".to_string()
+ } else if loading_count > 0 {
+ format!("Loading feeds... ({} loading, {} loaded, {} errors)
+
+ā³ = Loading
+ā = Loaded (episode count)
+ā = Error
+
+Navigation:
+⢠j/k or ā/ā - navigate
+⢠Enter - select feed
+⢠r - refresh feeds
+⢠q - quit", loading_count, loaded_count, error_count)
} else {
- "Navigation:\n⢠j/k or ā/ā - navigate\n⢠Enter - select feed\n⢠r - refresh feeds\n⢠q - quit"
+ "Navigation:
+⢠j/k or ā/ā - navigate
+⢠Enter - select feed
+⢠r - refresh feeds
+⢠q - quit".to_string()
};
let info = Paragraph::new(info_text)