1use crate::cmus::{TemplateProcessor, Track};
2#[cfg(feature = "debug")]
3use log::{debug, info};
4use std::path::Path;
5
6pub mod cmus;
7pub mod notification;
8pub mod settings;
9
10pub fn get_embedded_art(track_path: &str) -> std::io::Result<Option<image::DynamicImage>> {
38 let tags = id3::Tag::read_from_path(track_path)
39 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
40 let Some(picture) = tags.pictures().next() else { return Ok(None); };
41 Ok(Some(image::load_from_memory(&picture.data).map_err(
42 |e| std::io::Error::new(std::io::ErrorKind::Other, e),
43 )?))
44}
45
46pub fn search_for(
70 search_directory: &str,
71 mut max_depth: u8,
72 regx: ®ex::Regex,
73) -> std::io::Result<Option<String>> {
74 let mut search_directory = if Path::new(search_directory).is_file() {
75 #[cfg(feature = "debug")]
76 {
77 info!("The provided search directory is a file, searching in the parent directory.");
78 }
79 let Some(parent) = Path::new(search_directory).parent() else { return Ok(None); };
80 let Some(parent) = parent.to_str() else { return Ok(None); };
81 parent
82 } else {
83 search_directory
84 };
85 #[cfg(feature = "debug")]
86 {
87 info!("Searching for a file that matches the regular {regx:?} expression in \"{search_directory}\" and its subdirectories.");
88 info!("Max depth: {max_depth}");
89 }
90
91 loop {
92 if let Some(path) = search(search_directory, regx)? {
93 return Ok(Some(path));
94 }
95
96 if max_depth == 0 {
97 break Ok(None);
98 } else {
99 #[cfg(feature = "debug")]
100 {
101 info!("Could not find a file that matches the regular {regx:?} expression in \"{search_directory}\", searching in the parent directory.");
102 info!("Max depth: {max_depth}");
103 }
104 max_depth -= 1;
106 search_directory = {
107 let Some(parent) = Path::new(search_directory).parent() else { return Ok(None); };
108 let Some(parent) = parent.to_str() else { return Ok(None); };
109 parent
110 };
111 }
112 }
113}
114
115pub struct CompleteStr {
116 pub template: String,
117 pub str: String
118}
119
120#[derive(PartialEq)]
122#[cfg_attr(any(feature = "debug", test), derive(Debug))]
123pub enum TrackCover {
124 Embedded(image::DynamicImage),
127 External(String),
130 None,
132}
133
134impl TrackCover {
135 pub fn set_notification_image(&self, notification: &mut notify_rust::Notification) {
136 use TrackCover::*;
137 match self {
138 Embedded(cover) => {
139 #[cfg(feature = "debug")]
140 debug!("Setting the cover as the notification image.");
141 let Ok(image) = notify_rust::Image::try_from(cover.clone()) else { return; };
142 notification.image_data(image);
143 }
144 External(path) => {
145 #[cfg(feature = "debug")]
146 debug!("Setting the cover as the notification image.");
147 notification.image_path(path);
148 }
149 None => {
150 #[cfg(feature = "debug")]
151 debug!("The track does not have a cover.");
152 }
153 }
154 }
155}
156
157#[inline]
162pub fn track_cover(
163 mut path: String,
164 track_name: &str,
165 max_depth: u8,
166 force_use_external_cover: bool,
167 no_use_external_cover: bool,
168) -> TrackCover {
169 if !force_use_external_cover {
170 #[cfg(feature = "debug")]
171 info!("Trying to get the embedded cover of \"{path}\".");
172 if let Ok(Some(cover)) = get_embedded_art(&path) {
173 return TrackCover::Embedded(cover);
174 }
175 }
176
177 if !no_use_external_cover {
178 let (Ok(regx), path) = (match path.split('/').last() {
179 Some(last_pat) if last_pat.contains("r#") => {
180 (regex::Regex::new(&last_pat.replace("r#", "")),
181 path.remove(path.len() - last_pat.len() - 1).to_string())
183 }
184 _ => (regex::Regex::new(&format!(r"({track_name}).*\.(jpg|jpeg|png|gif)$")), path),
185 }) else {
186 #[cfg(feature = "debug")]
187 info!("Could not get the cover.");
188 return TrackCover::None;
189 };
190 #[cfg(feature = "debug")]
191 info!("Trying to get the external cover of \"{path}\".");
192 if let Ok(Some(cover)) = search_for(&path, max_depth, ®x) {
193 #[cfg(feature = "debug")]
194 info!("Found the external cover \"{cover}\".");
195 return TrackCover::External(cover);
196 }
197 }
198
199 #[cfg(feature = "debug")]
200 info!("Could not get the cover.");
201
202 TrackCover::None
203}
204
205#[inline]
206fn search(search_directory: &str, matcher: ®ex::Regex) -> std::io::Result<Option<String>> {
207 for entry in std::fs::read_dir(search_directory)? {
208 let Ok(entry) = entry else { continue; };
209 let Ok(file_type) = entry.file_type() else { continue; };
210 if file_type.is_file() {
211 let Ok(file_name) = entry.file_name().into_string() else { continue; };
212 if matcher.is_match(&file_name) {
214 let path = entry.path();
215 let Some(path) = path.to_str() else { continue; };
216 return Ok(Some(path.to_string()));
217 }
218 }
219 }
220 Ok(None)
221}
222
223#[inline(always)]
225pub fn process_template_placeholders(
226 template: String,
227 track: &Track,
228 player_settings: &cmus::player_settings::PlayerSettings,
229) -> String {
230 let res = track.process(template);
231 player_settings.process(res)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::cmus::player_settings::PlayerSettings;
238 use std::str::FromStr;
239 use test_context::{test_context, TestContext};
240
241 struct TestContextWithFullTrack {
242 track: Track,
243 player_settings: PlayerSettings,
244 }
245
246 impl TestContext for TestContextWithFullTrack {
247 fn setup() -> Self {
248 Self {
249 track: cmus::Track::from_str(include_str!(
250 "../tests/samples/cmus-remote-output-with-all-tags.txt"
251 ))
252 .unwrap(),
253 player_settings: PlayerSettings::from_str(include_str!(
254 "../tests/samples/player_settings_mode-artist_vol-46_repeat-false_repeat_current-false_shuffle-tracks.txt"
255 ))
256 .unwrap(),
257 }
258 }
259 }
260
261 #[test_context(TestContextWithFullTrack)]
262 #[test]
263 fn test_process_path_template(ctx: &TestContextWithFullTrack) {
264 let cover_path_template = String::from("{title}/{artist}/{album}/{tracknumber}");
265 let cover_path =
266 process_template_placeholders(cover_path_template, &ctx.track, &ctx.player_settings);
267
268 assert_eq!(
269 cover_path,
270 "Photograph/Alex Goot/Alex Goot & Friends, Vol. 3/8"
271 );
272 }
273
274 #[test]
275 fn test_search_for_cover_with_the_cover_key_world() {
276 let cover_path = search_for(
277 "tests/samples/Owl City/Cinematic/cover",
278 1,
279 ®ex::Regex::new(r"cover|.\.jpg|.\.png").unwrap(),
280 );
281
282 assert!(cover_path.is_ok());
284 let cover_path = cover_path.unwrap();
285 assert!(cover_path.is_some());
286 assert_eq!(
287 cover_path.unwrap(),
288 "tests/samples/Owl City/Cinematic/cover/cover.jpg"
289 );
290 }
291
292 #[test]
293 fn test_search_for_cover_without_the_cover_key_world() {
294 let cover_path = search_for(
295 "tests/samples/Owl City/Cinematic/cover",
296 1,
297 ®ex::Regex::new(r".\.jpg|.\.png").unwrap(),
298 );
299
300 assert!(cover_path.is_ok());
302 let cover_path = cover_path.unwrap();
303 assert!(cover_path.is_some());
304 assert_eq!(
305 cover_path.unwrap(),
306 "tests/samples/Owl City/Cinematic/cover/cover.jpg"
307 );
308 }
309
310 #[test]
311 fn test_search_for_cover_without_the_cover_key_world_and_jpg() {
312 let cover_path = search_for(
313 "tests/samples/Owl City/Cinematic/cover",
314 1,
315 ®ex::Regex::new(r".\.png").unwrap(),
316 );
317
318 assert!(cover_path.is_ok());
320 let cover_path = cover_path.unwrap();
321 assert!(cover_path.is_some());
322 assert_eq!(
323 cover_path.unwrap(),
324 "tests/samples/Owl City/Cinematic/cover/cover.png"
325 );
326 }
327
328 #[test]
329 fn test_search_for_lrc_file_started_from_the_cover_directory() {
330 let lrc_path = search_for(
331 "tests/samples/Owl City/Cinematic/cover",
332 1,
333 ®ex::Regex::new(r".\.lrc").unwrap(),
334 );
335
336 assert!(lrc_path.is_ok());
338 let lrc_path = lrc_path.unwrap();
339 assert!(lrc_path.is_some());
340 assert_eq!(
341 lrc_path.unwrap(),
342 "tests/samples/Owl City/Cinematic/08 - Always.lrc"
343 );
344 }
345
346 #[test]
347 fn test_search_for_not_exits_file() {
348 let result = search_for(
349 "tests/samples/Owl City/Cinematic/cover",
350 3,
351 ®ex::Regex::new(r".\.mp3").unwrap(),
352 );
353
354 assert!(result.is_ok());
356 let result = result.unwrap();
357 assert!(result.is_none());
358 }
359}