Skip to content

Commit 4c056bb

Browse files
author
Ross
committed
Dry up some of the code in app
1 parent cf9e199 commit 4c056bb

10 files changed

Lines changed: 1161 additions & 1235 deletions

File tree

src/app.rs

Lines changed: 120 additions & 1131 deletions
Large diffs are not rendered by default.

src/app/cover_art.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use std::io::Cursor;
2+
3+
use image::DynamicImage;
4+
use ratatui_image::picker::Picker;
5+
use anyhow::Result;
6+
7+
use crate::app::Track;
8+
9+
use super::App;
10+
impl App {
11+
pub fn sanitize_album_name(&self, name: &str) -> String {
12+
name.replace(|c: char| !c.is_alphanumeric() && c != '-', "_")
13+
.to_lowercase()
14+
.chars()
15+
.fold(String::new(), |mut acc, c| {
16+
if c == '_' && acc.ends_with('_') {
17+
acc
18+
} else {
19+
acc.push(c);
20+
acc
21+
}
22+
})
23+
}
24+
pub async fn load_cover_art_for_track(&mut self, track: &Track) {
25+
self.cover_art_protocol = None;
26+
let album = self.sanitize_album_name(&track.album);
27+
28+
let url = match &track.cover_art {
29+
Some(url) if !url.is_empty() => url,
30+
_ => return,
31+
};
32+
let mut cache_path = std::env::temp_dir();
33+
cache_path.push("sonicrust");
34+
cache_path.push(format!("cover_{}.jpg", album));
35+
let img_result = if cache_path.exists() {
36+
log::debug!("Using cached cover_art for {}", album);
37+
image::open(&cache_path)
38+
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
39+
} else {
40+
self.fetch_cover_art(url).await.inspect(|img| {
41+
let _ = std::fs::create_dir_all(cache_path.parent().unwrap());
42+
let _ = img.save(&cache_path);
43+
})
44+
};
45+
match img_result {
46+
Ok(img) => match Picker::from_query_stdio() {
47+
Ok(picker) => {
48+
self.cover_art_protocol = Some(picker.new_resize_protocol(img));
49+
}
50+
Err(e) => log::debug!("Failed ot create image picker: {}", e),
51+
},
52+
Err(e) => eprintln!("failed to load cover art: {}", e),
53+
}
54+
}
55+
pub async fn fetch_and_cache_image(&self, url: &str, track_album: &str) -> Result<String> {
56+
log::debug!("Currently fetching cover are for album: {}", track_album,);
57+
let mut path = std::env::temp_dir();
58+
path.push("sonicrust");
59+
std::fs::create_dir_all(&path).map_err(|e| anyhow::anyhow!(e))?;
60+
path.push(format!("cover_{}.jpg", track_album));
61+
if path.exists() {
62+
log::debug!("Using cached cover art for {}", track_album,);
63+
return Ok(path.to_string_lossy().to_string());
64+
}
65+
log::debug!("fetching cover art for {}", track_album);
66+
let img = self
67+
.fetch_cover_art(url)
68+
.await
69+
.map_err(|e| anyhow::anyhow!("Failed to fetch cover art: {}", e))?;
70+
img.save(&path).map_err(|e| anyhow::anyhow!(e))?;
71+
72+
Ok(path.to_string_lossy().to_string())
73+
}
74+
75+
async fn fetch_cover_art(
76+
&self,
77+
url: &str,
78+
) -> Result<DynamicImage, Box<dyn std::error::Error + Send + Sync>> {
79+
let url = url.to_string();
80+
tokio::task::spawn_blocking(move || {
81+
let res = reqwest::blocking::get(&url)?;
82+
if !res.status().is_success() {
83+
return Err(format!("HTTP error: {}", res.status()).into());
84+
}
85+
let bytes = res.bytes()?;
86+
if bytes.is_empty() {
87+
return Err("Empty response when fetching cover art".into());
88+
}
89+
let format = image::guess_format(&bytes).unwrap_or(image::ImageFormat::Jpeg);
90+
let img = image::load(Cursor::new(bytes), format)?;
91+
Ok(img)
92+
})
93+
.await?
94+
}
95+
pub fn _clear_cover_art_cache() -> Result<()> {
96+
let mut path = std::env::temp_dir();
97+
path.push("sonicrust");
98+
if path.exists() {
99+
std::fs::remove_dir_all(&path)?;
100+
}
101+
Ok(())
102+
}
103+
}

src/app/input.rs

Whitespace-only changes.

src/app/mpris.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use mpris_server::{PlaybackStatus, Property};
2+
use notify_rust::{Hint, Notification};
3+
use anyhow::Result;
4+
5+
use crate::app::Track;
6+
7+
use super::App;
8+
impl App {
9+
pub async fn sync_mpris(&mut self) {
10+
let status = if self.is_playing {
11+
PlaybackStatus::Playing
12+
} else if self.current_track.is_some() {
13+
PlaybackStatus::Paused
14+
} else {
15+
PlaybackStatus::Stopped
16+
};
17+
18+
let can_next = self.queue_tab.index < self.queue_tab.len().saturating_sub(1);
19+
let can_prev = self.queue_tab.index > 0;
20+
let current_pos = self.player.lock().await.get_position();
21+
22+
if let Ok(mut state) = self.shared_state.write() {
23+
state.status = status;
24+
state.metadata = self.metadata.clone();
25+
state.can_go_next = can_next;
26+
state.can_go_previous = can_prev;
27+
state.position = current_pos;
28+
}
29+
30+
let _ = self
31+
.mpris
32+
.properties_changed([
33+
Property::PlaybackStatus(status),
34+
Property::Metadata(self.metadata.clone()),
35+
Property::CanGoNext(can_next),
36+
Property::CanGoPrevious(can_prev),
37+
])
38+
.await;
39+
}
40+
41+
pub async fn notify_now_playing(&mut self, track: &Track) -> Result<()> {
42+
let mut notif = Notification::new()
43+
.appname("Sonicrust")
44+
.summary("Now playing")
45+
.body(format!("{} - {}", track.title, track.artist).as_str())
46+
.finalize();
47+
if let Some(url) = &track.cover_art
48+
&& !url.is_empty()
49+
{
50+
let album = self.sanitize_album_name(&track.album);
51+
52+
match self.fetch_and_cache_image(url, &album).await {
53+
Ok(path) => {
54+
notif.hint(Hint::ImagePath(path));
55+
}
56+
Err(e) => {
57+
log::debug!("Could not load cover art for notification: {}", e);
58+
}
59+
}
60+
}
61+
let _ = notif.show();
62+
Ok(())
63+
}
64+
pub async fn update_mpris_position(&mut self) -> Result<()> {
65+
if self.is_playing {
66+
let current_pos = self.player.lock().await.get_position();
67+
if let Ok(mut state) = self.shared_state.write() {
68+
state.position = current_pos;
69+
}
70+
}
71+
Ok(())
72+
}
73+
}

src/app/navigation.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use crate::app::{ActiveSection, ActiveTab, InputMode};
2+
3+
use super::App;
4+
macro_rules! navigate_list {
5+
($state:expr, $forward:expr) => {
6+
let len = $state.data.len();
7+
if len > 0 {
8+
let i = if $forward {
9+
$state.state.selected().map_or(0, |s| (s + 1) % len)
10+
} else {
11+
$state
12+
.state
13+
.selected()
14+
.map_or(len - 1, |s| if s == 0 { len - 1 } else { s - 1 })
15+
};
16+
$state.index = i;
17+
$state.state.select(Some(i));
18+
} else {
19+
$state.state.select(None)
20+
}
21+
};
22+
}
23+
24+
impl App {
25+
pub fn select_tab(&mut self, tab: ActiveTab) {
26+
match self.active_tab {
27+
// ActiveTab::Queue => self.queue_state.select(None),
28+
ActiveTab::Songs => self.tracks_tab.clear(),
29+
ActiveTab::Playlist => self.playlist_tab.clear(),
30+
ActiveTab::Artists => self.artist_tab.clear(),
31+
ActiveTab::Albums => self.album_tab.clear(),
32+
ActiveTab::Favorites => self.favorite_tab.clear(),
33+
ActiveTab::Search => {
34+
self.search_tab.clear();
35+
self.input_mode = InputMode::Normal;
36+
}
37+
}
38+
39+
self.active_tab = tab.clone();
40+
41+
// Initialize new tab state
42+
match tab {
43+
ActiveTab::Playlist if !self.playlist_tab.data.is_empty() => {
44+
self.playlist_tab.current();
45+
}
46+
ActiveTab::Songs if !self.tracks_tab.data.is_empty() => {
47+
self.tracks_tab.current();
48+
}
49+
ActiveTab::Artists if !self.artist_tab.data.is_empty() => {
50+
self.artist_tab.current();
51+
}
52+
ActiveTab::Favorites if !self.favorite_tab.data.is_empty() => {
53+
self.favorite_tab.current();
54+
}
55+
ActiveTab::Albums if !self.album_tab.data.is_empty() => {
56+
self.album_tab.current();
57+
}
58+
ActiveTab::Search if !self.search_tab.data.is_empty() => {
59+
self.search_tab.current();
60+
}
61+
_ => {}
62+
}
63+
}
64+
pub fn next_tab(&mut self) {
65+
self.active_section = match self.active_section {
66+
ActiveSection::Queue => ActiveSection::Others,
67+
ActiveSection::Others => ActiveSection::Queue,
68+
};
69+
match self.active_section {
70+
ActiveSection::Queue => {
71+
if !self.queue_tab.data.is_empty() {
72+
self.queue_tab.current();
73+
}
74+
}
75+
ActiveSection::Others => match self.active_tab {
76+
ActiveTab::Songs => {
77+
self.artist_tab.select(self.tracks_tab.index);
78+
}
79+
ActiveTab::Favorites => {
80+
self.favorite_tab.current();
81+
}
82+
ActiveTab::Playlist => {
83+
self.playlist_tab.current();
84+
}
85+
ActiveTab::Artists => {
86+
self.album_tab.select(self.artist_tab.index);
87+
}
88+
ActiveTab::Albums => {
89+
self.tracks_tab.select(self.album_tab.index);
90+
}
91+
ActiveTab::Search => {
92+
self.tracks_tab.select(self.search_tab.index);
93+
}
94+
},
95+
};
96+
}
97+
pub fn previous_tab(&mut self) {
98+
self.next_tab();
99+
}
100+
pub fn next_item_in_tab(&mut self) {
101+
match self.active_section {
102+
ActiveSection::Queue => {
103+
navigate_list!(self.queue_tab, true);
104+
}
105+
ActiveSection::Others => match self.active_tab {
106+
ActiveTab::Search => {
107+
if !self.search_tab.data.is_empty() {
108+
navigate_list!(self.search_tab, true);
109+
}
110+
}
111+
ActiveTab::Playlist => {
112+
if !self.playlist_tab.data.is_empty() {
113+
navigate_list!(self.playlist_tab, true);
114+
}
115+
}
116+
ActiveTab::Songs => {
117+
if !self.tracks_tab.data.is_empty() {
118+
navigate_list!(self.tracks_tab, true);
119+
}
120+
}
121+
ActiveTab::Favorites => {
122+
if !self.favorite_tab.data.is_empty() {
123+
navigate_list!(self.favorite_tab, true);
124+
}
125+
}
126+
ActiveTab::Artists => {
127+
if !self.artist_tab.data.is_empty() {
128+
navigate_list!(self.artist_tab, true);
129+
}
130+
}
131+
ActiveTab::Albums => {
132+
if !self.album_tab.data.is_empty() {
133+
navigate_list!(self.album_tab, true);
134+
}
135+
}
136+
},
137+
}
138+
}
139+
pub fn previous_item_in_tab(&mut self) {
140+
match self.active_section {
141+
ActiveSection::Queue => {
142+
if !self.queue_tab.data.is_empty() {
143+
navigate_list!(self.queue_tab, false);
144+
}
145+
}
146+
ActiveSection::Others => match self.active_tab {
147+
ActiveTab::Favorites => {
148+
if !self.favorite_tab.data.is_empty() {
149+
navigate_list!(self.favorite_tab, false);
150+
}
151+
}
152+
ActiveTab::Playlist => {
153+
if !self.playlist_tab.data.is_empty() {
154+
navigate_list!(self.playlist_tab, false);
155+
}
156+
}
157+
ActiveTab::Search => {
158+
if !self.search_tab.data.is_empty() {
159+
navigate_list!(self.search_tab, false);
160+
}
161+
}
162+
ActiveTab::Songs => {
163+
if !self.tracks_tab.data.is_empty() {
164+
navigate_list!(self.tracks_tab, false);
165+
}
166+
}
167+
ActiveTab::Artists => {
168+
if !self.artist_tab.data.is_empty() {
169+
navigate_list!(self.artist_tab, false);
170+
}
171+
}
172+
ActiveTab::Albums => {
173+
if !self.album_tab.data.is_empty() {
174+
navigate_list!(self.album_tab, false);
175+
}
176+
}
177+
},
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)