main
  1use crate::{config::Config, feed::Feed, player::Player};
  2use anyhow::Result;
  3use std::sync::mpsc;
  4use std::thread;
  5
  6#[derive(Debug, Clone, Copy, PartialEq)]
  7pub enum CurrentScreen {
  8    FeedList,
  9    EpisodeList,
 10    NowPlaying,
 11    MusicDiscovery,
 12}
 13
 14pub struct App {
 15    pub current_screen: CurrentScreen,
 16    pub feeds: Vec<Feed>,
 17    pub selected_feed: usize,
 18    pub selected_episode: usize,
 19    pub _config: Config,
 20    pub player: Player,
 21    pub current_track: Option<CurrentTrack>,
 22    pub volume: u8,
 23    pub selected_genre: usize,
 24    pub cjsw_shows: Vec<CjswShow>,
 25    feed_receiver: mpsc::Receiver<(usize, Feed)>,
 26}
 27
 28#[derive(Debug, Clone)]
 29pub struct CurrentTrack {
 30    pub title: String,
 31    pub podcast_name: String,
 32    pub _duration: Option<String>,
 33    pub _position: f64, // seconds
 34}
 35
 36#[derive(Debug, Clone)]
 37pub struct CjswShow {
 38    pub name: String,
 39    pub genre: String,
 40    pub time_slot: String,
 41    pub description: String,
 42    pub day: String,
 43}
 44
 45impl App {
 46    pub fn new() -> Result<Self> {
 47        let config = Config::load()?;
 48        let feeds: Vec<Feed> = config
 49            .feeds
 50            .iter()
 51            .map(|(name, url)| Feed::new(name.clone(), url.clone()))
 52            .collect();
 53
 54        // Set up async feed loading
 55        let (sender, receiver) = mpsc::channel();
 56
 57        // Start background feed loading
 58        for (index, (name, url)) in config.feeds.iter().enumerate() {
 59            let sender = sender.clone();
 60            let name = name.clone();
 61            let url = url.clone();
 62
 63            thread::spawn(move || {
 64                let mut feed = Feed::new(name, url);
 65                let _ = feed.fetch_episodes(); // Ignore errors for now, state tracks them
 66                let _ = sender.send((index, feed));
 67            });
 68        }
 69
 70        Ok(Self {
 71            current_screen: CurrentScreen::FeedList,
 72            feeds,
 73            selected_feed: 0,
 74            selected_episode: 0,
 75            _config: config,
 76            player: Player::new()?,
 77            current_track: None,
 78            volume: 70, // Default volume 70%
 79            selected_genre: 0,
 80            cjsw_shows: Self::load_cjsw_shows(),
 81            feed_receiver: receiver,
 82        })
 83    }
 84
 85    pub fn next_feed(&mut self) {
 86        if !self.feeds.is_empty() {
 87            self.selected_feed = (self.selected_feed + 1) % self.feeds.len();
 88        }
 89    }
 90
 91    pub fn previous_feed(&mut self) {
 92        if !self.feeds.is_empty() {
 93            self.selected_feed = if self.selected_feed == 0 {
 94                self.feeds.len() - 1
 95            } else {
 96                self.selected_feed - 1
 97            };
 98        }
 99    }
100
101    pub fn next_episode(&mut self) {
102        if let Some(feed) = self.feeds.get(self.selected_feed) {
103            if !feed.episodes.is_empty() {
104                self.selected_episode = (self.selected_episode + 1) % feed.episodes.len();
105            }
106        }
107    }
108
109    pub fn previous_episode(&mut self) {
110        if let Some(feed) = self.feeds.get(self.selected_feed) {
111            if !feed.episodes.is_empty() {
112                self.selected_episode = if self.selected_episode == 0 {
113                    feed.episodes.len() - 1
114                } else {
115                    self.selected_episode - 1
116                };
117            }
118        }
119    }
120
121    pub fn select_feed(&mut self) {
122        if !self.feeds.is_empty() {
123            self.current_screen = CurrentScreen::EpisodeList;
124            self.selected_episode = 0;
125        }
126    }
127
128    pub fn back_to_feeds(&mut self) {
129        self.current_screen = CurrentScreen::FeedList;
130    }
131
132    pub fn refresh_feeds(&mut self) -> Result<()> {
133        for feed in &mut self.feeds {
134            if let Err(e) = feed.fetch_episodes() {
135                // Log error but continue with other feeds
136                eprintln!("Failed to fetch episodes for {}: {}", feed.name, e);
137            }
138        }
139        Ok(())
140    }
141
142    pub fn play_episode(&mut self) -> Result<()> {
143        if let Some(feed) = self.feeds.get(self.selected_feed) {
144            if let Some(episode) = feed.episodes.get(self.selected_episode) {
145                // Prioritize cached episodes for instant playback
146                if episode.is_cached && episode.local_path.is_some() {
147                    // Play from local file instantly
148                    let local_path = episode.local_path.as_ref().unwrap();
149                    self.player.play_from_local_file(local_path)?;
150                } else {
151                    // Download and cache if not cached
152                    self.player.play_with_cache(
153                        &episode.enclosure_url,
154                        Some((&feed.name, &episode.title)),
155                    )?;
156                }
157
158                // Set current track info and switch to NowPlaying screen
159                self.current_track = Some(CurrentTrack {
160                    title: episode.title.clone(),
161                    podcast_name: feed.name.clone(),
162                    _duration: episode._duration.clone(),
163                    _position: 0.0,
164                });
165                self.current_screen = CurrentScreen::NowPlaying;
166            }
167        }
168        Ok(())
169    }
170
171    // Media control methods
172    pub fn toggle_playback(&mut self) -> Result<()> {
173        self.player.toggle_pause()
174    }
175
176    pub fn volume_up(&mut self) -> Result<()> {
177        if self.volume < 100 {
178            self.volume = (self.volume + 5).min(100);
179            self.player.set_volume(self.volume)
180        } else {
181            Ok(())
182        }
183    }
184
185    pub fn volume_down(&mut self) -> Result<()> {
186        if self.volume > 0 {
187            self.volume = self.volume.saturating_sub(5);
188            self.player.set_volume(self.volume)
189        } else {
190            Ok(())
191        }
192    }
193
194    pub fn seek_forward(&mut self) -> Result<()> {
195        self.player.skip_forward(15.0) // 15 seconds forward (common podcast increment)
196    }
197
198    pub fn seek_backward(&mut self) -> Result<()> {
199        self.player.skip_backward(15.0) // 15 seconds backward
200    }
201
202    pub fn skip_forward_long(&mut self) -> Result<()> {
203        self.player.skip_forward(60.0) // 1 minute forward
204    }
205
206    pub fn skip_backward_long(&mut self) -> Result<()> {
207        self.player.skip_backward(60.0) // 1 minute backward
208    }
209
210    pub fn stop_playback(&mut self) {
211        self.player.stop();
212        self.current_track = None;
213        self.current_screen = CurrentScreen::EpisodeList;
214    }
215
216    pub fn back_to_episodes(&mut self) {
217        self.current_screen = CurrentScreen::EpisodeList;
218    }
219
220    // Check for and process incoming feed updates
221    pub fn update_feeds(&mut self) {
222        // Process all available feed updates without blocking
223        while let Ok((index, feed)) = self.feed_receiver.try_recv() {
224            if index < self.feeds.len() {
225                self.feeds[index] = feed;
226            }
227        }
228    }
229
230    // Music discovery navigation
231    pub fn go_to_music_discovery(&mut self) {
232        self.current_screen = CurrentScreen::MusicDiscovery;
233        self.selected_genre = 0;
234    }
235
236    pub fn next_genre(&mut self) {
237        if !self.cjsw_shows.is_empty() {
238            let unique_genres = self.get_unique_genres();
239            if !unique_genres.is_empty() {
240                self.selected_genre = (self.selected_genre + 1) % unique_genres.len();
241            }
242        }
243    }
244
245    pub fn previous_genre(&mut self) {
246        if !self.cjsw_shows.is_empty() {
247            let unique_genres = self.get_unique_genres();
248            if !unique_genres.is_empty() {
249                self.selected_genre = if self.selected_genre == 0 {
250                    unique_genres.len() - 1
251                } else {
252                    self.selected_genre - 1
253                };
254            }
255        }
256    }
257
258    pub fn get_unique_genres(&self) -> Vec<String> {
259        let mut genres: Vec<String> = self.cjsw_shows
260            .iter()
261            .map(|show| show.genre.clone())
262            .collect::<std::collections::HashSet<_>>()
263            .into_iter()
264            .collect();
265        genres.sort();
266        genres
267    }
268
269    pub fn get_shows_by_genre(&self, genre: &str) -> Vec<&CjswShow> {
270        self.cjsw_shows
271            .iter()
272            .filter(|show| show.genre == genre)
273            .collect()
274    }
275
276    // Load CJSW shows data
277    fn load_cjsw_shows() -> Vec<CjswShow> {
278        vec![
279            CjswShow {
280                name: "Black Milk".to_string(),
281                genre: "Electronic/Experimental".to_string(),
282                time_slot: "Monday 1:00-3:00 AM".to_string(),
283                description: "Electronic and experimental music exploration".to_string(),
284                day: "Monday".to_string(),
285            },
286            CjswShow {
287                name: "Soular Power".to_string(),
288                genre: "R&B/Soul".to_string(),
289                time_slot: "Tuesday 7:00-9:00 PM".to_string(),
290                description: "Classic and contemporary R&B and soul".to_string(),
291                day: "Tuesday".to_string(),
292            },
293            CjswShow {
294                name: "Fade to Bass".to_string(),
295                genre: "Electronic".to_string(),
296                time_slot: "Friday 11:00 PM-1:00 AM".to_string(),
297                description: "House and techno music journey".to_string(),
298                day: "Friday".to_string(),
299            },
300            CjswShow {
301                name: "Noise".to_string(),
302                genre: "Experimental".to_string(),
303                time_slot: "Wednesday 2:00-4:00 AM".to_string(),
304                description: "30+ years of avant-garde experimental music".to_string(),
305                day: "Wednesday".to_string(),
306            },
307            CjswShow {
308                name: "Local Singles".to_string(),
309                genre: "Local/Indie".to_string(),
310                time_slot: "Thursday 6:00-8:00 PM".to_string(),
311                description: "Featuring Calgary and Alberta local artists".to_string(),
312                day: "Thursday".to_string(),
313            },
314            CjswShow {
315                name: "CantoStars".to_string(),
316                genre: "Multicultural".to_string(),
317                time_slot: "Sunday 10:00 AM-12:00 PM".to_string(),
318                description: "Cantonese music and cultural programming".to_string(),
319                day: "Sunday".to_string(),
320            },
321            CjswShow {
322                name: "Sonic Cycle".to_string(),
323                genre: "Indie Pop/Rock".to_string(),
324                time_slot: "Saturday 3:00-5:00 PM".to_string(),
325                description: "Genre-blending indie music journey".to_string(),
326                day: "Saturday".to_string(),
327            },
328            CjswShow {
329                name: "Jazz Spectrum".to_string(),
330                genre: "Jazz".to_string(),
331                time_slot: "Monday 8:00-10:00 PM".to_string(),
332                description: "Classic to contemporary jazz exploration".to_string(),
333                day: "Monday".to_string(),
334            },
335        ]
336    }
337}