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}