format the code :)

This commit is contained in:
Anas Elgarhy 2023-02-08 21:12:43 +02:00
parent be412d5f57
commit a5dd196937
No known key found for this signature in database
GPG key ID: 0501802A1D496528
4 changed files with 177 additions and 57 deletions

View file

@ -1,7 +1,8 @@
use clap::Parser; use clap::Parser;
const NOTIFICATION_TIMEOUT: u8 = 5; const NOTIFICATION_TIMEOUT: u8 = 5;
const NOTIFICATION_BODY: &str = "<b>Playing:</b> {title} from {album} \n\n <b>Artist:</b> {artist} - {year}"; const NOTIFICATION_BODY: &str =
"<b>Playing:</b> {title} from {album} \n\n <b>Artist:</b> {artist} - {year}";
const NOTIFICATION_SUMMARY: &str = "{artist} - {title}"; const NOTIFICATION_SUMMARY: &str = "{artist} - {title}";
const NOTIFICATION_APP_NAME: &str = "C* Music Player"; const NOTIFICATION_APP_NAME: &str = "C* Music Player";
const DEFAULT_MAX_DEPTH: u8 = 3; const DEFAULT_MAX_DEPTH: u8 = 3;

View file

@ -34,7 +34,7 @@ pub enum CmusError {
EmptyPath, EmptyPath,
DurationError(String), DurationError(String),
PositionError(String), PositionError(String),
UnknownError(String) UnknownError(String),
} }
impl Display for CmusError { impl Display for CmusError {
@ -75,21 +75,43 @@ impl FromStr for Track {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut lines = s.lines(); let mut lines = s.lines();
Ok(Track::builder().status( Ok(Track::builder()
TrackStatus::from_str(lines.next().ok_or(CmusError::NoStatus)?.split_once(' ') .status(TrackStatus::from_str(
.ok_or(CmusError::NoStatus)?.1)? lines
) .next()
.path(lines.next().ok_or(CmusError::EmptyPath)?.split_once(' ') .ok_or(CmusError::NoStatus)?
.ok_or(CmusError::EmptyPath)?.1.to_string()) .split_once(' ')
.ok_or(CmusError::NoStatus)?
.1,
)?)
.path(
lines
.next()
.ok_or(CmusError::EmptyPath)?
.split_once(' ')
.ok_or(CmusError::EmptyPath)?
.1
.to_string(),
)
.duration( .duration(
lines.next().ok_or(CmusError::DurationError("Missing duration".to_string()))?.split_once(' ') lines
.ok_or(CmusError::DurationError("Empty duration".to_string()))?.1.parse() .next()
.map_err(|e: ParseIntError| CmusError::DurationError(e.to_string()))? .ok_or(CmusError::DurationError("Missing duration".to_string()))?
.split_once(' ')
.ok_or(CmusError::DurationError("Empty duration".to_string()))?
.1
.parse()
.map_err(|e: ParseIntError| CmusError::DurationError(e.to_string()))?,
) )
.position( .position(
lines.next().ok_or(CmusError::PositionError("Missing position".to_string()))?.split_once(' ') lines
.ok_or(CmusError::PositionError("Empty position".to_string()))?.1.parse() .next()
.map_err(|e: ParseIntError| CmusError::PositionError(e.to_string()))? .ok_or(CmusError::PositionError("Missing position".to_string()))?
.split_once(' ')
.ok_or(CmusError::PositionError("Empty position".to_string()))?
.1
.parse()
.map_err(|e: ParseIntError| CmusError::PositionError(e.to_string()))?,
) )
.metadata(TrackMetadata::parse(lines)) .metadata(TrackMetadata::parse(lines))
.build()) .build())
@ -101,7 +123,7 @@ impl TrackMetadata {
/// This function will assume you processed the first 4 lines, and remove them from the iterator. /// This function will assume you processed the first 4 lines, and remove them from the iterator.
/// ///
/// and also assume the all tags is contained in the iterator. /// and also assume the all tags is contained in the iterator.
fn parse<'a>(mut lines: impl Iterator<Item=&'a str>) -> Self { fn parse<'a>(mut lines: impl Iterator<Item = &'a str>) -> Self {
let mut tags = HashMap::new(); let mut tags = HashMap::new();
while let Some(line) = lines.next() { while let Some(line) = lines.next() {
@ -129,8 +151,15 @@ impl Track {
/// ///
/// This is the title, if it exists, otherwise it's the file name without the extension. /// This is the title, if it exists, otherwise it's the file name without the extension.
pub fn get_name(&self) -> &str { pub fn get_name(&self) -> &str {
self.metadata.get("title").unwrap_or_else(|| self.path.split('/').last() self.metadata.get("title").unwrap_or_else(|| {
.unwrap_or("").split_once(".").unwrap_or(("", "")).0) self.path
.split('/')
.last()
.unwrap_or("")
.split_once(".")
.unwrap_or(("", ""))
.0
})
} }
} }
@ -140,14 +169,18 @@ impl Track {
#[inline] #[inline]
pub fn get_track(query_command: &mut std::process::Command) -> Result<Track, CmusError> { pub fn get_track(query_command: &mut std::process::Command) -> Result<Track, CmusError> {
// Just run the command, and collect the output. // Just run the command, and collect the output.
let output = query_command.output().map_err(|e| CmusError::CmusRunningError(e.to_string()))?; let output = query_command
.output()
.map_err(|e| CmusError::CmusRunningError(e.to_string()))?;
if !output.status.success() { if !output.status.success() {
return Err(CmusError::CmusRunningError(String::from_utf8(output.stderr) return Err(CmusError::CmusRunningError(
.map_err(|e| CmusError::UnknownError(e.to_string()))?)); String::from_utf8(output.stderr).map_err(|e| CmusError::UnknownError(e.to_string()))?,
));
} }
let output = String::from_utf8(output.stdout).map_err(|e| CmusError::UnknownError(e.to_string()))?; let output =
String::from_utf8(output.stdout).map_err(|e| CmusError::UnknownError(e.to_string()))?;
Track::from_str(&output).map_err(|e| CmusError::UnknownError(e.to_string())) Track::from_str(&output).map_err(|e| CmusError::UnknownError(e.to_string()))
} }
@ -156,7 +189,11 @@ pub fn get_track(query_command: &mut std::process::Command) -> Result<Track, Cmu
/// This function it should call only one time entire the program life time, So it makes sense to make it inline. /// This function it should call only one time entire the program life time, So it makes sense to make it inline.
/// This function will return a `std::process::Command` that can be used to query cmus, you should store it in a variable :). /// This function will return a `std::process::Command` that can be used to query cmus, you should store it in a variable :).
#[inline(always)] #[inline(always)]
pub fn build_query_command(cmus_remote_bin: &str, socket_addr: &Option<String>, socket_pass: &Option<String>) -> std::process::Command { pub fn build_query_command(
cmus_remote_bin: &str,
socket_addr: &Option<String>,
socket_pass: &Option<String>,
) -> std::process::Command {
let cmd_arr = cmus_remote_bin.split_whitespace().collect::<Vec<_>>(); let cmd_arr = cmus_remote_bin.split_whitespace().collect::<Vec<_>>();
let mut command = std::process::Command::new(cmd_arr[0]); let mut command = std::process::Command::new(cmd_arr[0]);
@ -178,13 +215,13 @@ pub fn build_query_command(cmus_remote_bin: &str, socket_addr: &Option<String>,
command command
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::assert_matches::assert_matches; use std::assert_matches::assert_matches;
const OUTPUT_WITH_ALL_TAGS: &str = include_str!("../../tests/samples/cmus-remote-output-with-all-tags.txt"); const OUTPUT_WITH_ALL_TAGS: &str =
include_str!("../../tests/samples/cmus-remote-output-with-all-tags.txt");
const SOME_TAGS: &str = r#"tag artist Alex Goot const SOME_TAGS: &str = r#"tag artist Alex Goot
tag album Alex Goot & Friends, Vol. 3 tag album Alex Goot & Friends, Vol. 3
@ -214,18 +251,48 @@ mod tests {
assert_eq!(track.path, "/mnt/Data/Music/FLAC/Alex Goot/Alex Goot - Alex Goot & Friends, Vol. 3/08 - Photograph.mp3"); assert_eq!(track.path, "/mnt/Data/Music/FLAC/Alex Goot/Alex Goot - Alex Goot & Friends, Vol. 3/08 - Photograph.mp3");
assert_eq!(track.duration, 284); assert_eq!(track.duration, 284);
assert_eq!(track.position, 226); assert_eq!(track.position, 226);
assert_eq!(track.metadata.tags.get("artist"), Some(&"Alex Goot".to_string())); assert_eq!(
assert_eq!(track.metadata.tags.get("album"), Some(&"Alex Goot & Friends, Vol. 3".to_string())); track.metadata.tags.get("artist"),
assert_eq!(track.metadata.tags.get("title"), Some(&"Photograph".to_string())); Some(&"Alex Goot".to_string())
);
assert_eq!(
track.metadata.tags.get("album"),
Some(&"Alex Goot & Friends, Vol. 3".to_string())
);
assert_eq!(
track.metadata.tags.get("title"),
Some(&"Photograph".to_string())
);
assert_eq!(track.metadata.tags.get("date"), Some(&"2014".to_string())); assert_eq!(track.metadata.tags.get("date"), Some(&"2014".to_string()));
assert_eq!(track.metadata.tags.get("genre"), Some(&"Pop".to_string())); assert_eq!(track.metadata.tags.get("genre"), Some(&"Pop".to_string()));
assert_eq!(track.metadata.tags.get("discnumber"), Some(&"1".to_string())); assert_eq!(
assert_eq!(track.metadata.tags.get("tracknumber"), Some(&"8".to_string())); track.metadata.tags.get("discnumber"),
assert_eq!(track.metadata.tags.get("albumartist"), Some(&"Alex Goot".to_string())); Some(&"1".to_string())
assert_eq!(track.metadata.tags.get("replaygain_track_gain"), Some(&"-9.4 dB".to_string())); );
assert_eq!(track.metadata.tags.get("composer"), Some(&"Chad Kroeger".to_string())); assert_eq!(
assert_eq!(track.metadata.tags.get("label"), Some(&"mudhutdigital.com".to_string())); track.metadata.tags.get("tracknumber"),
assert_eq!(track.metadata.tags.get("publisher"), Some(&"mudhutdigital.com".to_string())); Some(&"8".to_string())
);
assert_eq!(
track.metadata.tags.get("albumartist"),
Some(&"Alex Goot".to_string())
);
assert_eq!(
track.metadata.tags.get("replaygain_track_gain"),
Some(&"-9.4 dB".to_string())
);
assert_eq!(
track.metadata.tags.get("composer"),
Some(&"Chad Kroeger".to_string())
);
assert_eq!(
track.metadata.tags.get("label"),
Some(&"mudhutdigital.com".to_string())
);
assert_eq!(
track.metadata.tags.get("publisher"),
Some(&"mudhutdigital.com".to_string())
);
assert_eq!(track.metadata.tags.get("bpm"), Some(&"146".to_string())); assert_eq!(track.metadata.tags.get("bpm"), Some(&"146".to_string()));
} }
@ -234,17 +301,35 @@ mod tests {
let metadata = TrackMetadata::parse(SOME_TAGS.lines()); let metadata = TrackMetadata::parse(SOME_TAGS.lines());
assert_eq!(metadata.tags.get("artist"), Some(&"Alex Goot".to_string())); assert_eq!(metadata.tags.get("artist"), Some(&"Alex Goot".to_string()));
assert_eq!(metadata.tags.get("album"), Some(&"Alex Goot & Friends, Vol. 3".to_string())); assert_eq!(
metadata.tags.get("album"),
Some(&"Alex Goot & Friends, Vol. 3".to_string())
);
assert_eq!(metadata.tags.get("title"), Some(&"Photograph".to_string())); assert_eq!(metadata.tags.get("title"), Some(&"Photograph".to_string()));
assert_eq!(metadata.tags.get("date"), Some(&"2014".to_string())); assert_eq!(metadata.tags.get("date"), Some(&"2014".to_string()));
assert_eq!(metadata.tags.get("genre"), Some(&"Pop".to_string())); assert_eq!(metadata.tags.get("genre"), Some(&"Pop".to_string()));
assert_eq!(metadata.tags.get("discnumber"), Some(&"1".to_string())); assert_eq!(metadata.tags.get("discnumber"), Some(&"1".to_string()));
assert_eq!(metadata.tags.get("tracknumber"), Some(&"8".to_string())); assert_eq!(metadata.tags.get("tracknumber"), Some(&"8".to_string()));
assert_eq!(metadata.tags.get("albumartist"), Some(&"Alex Goot".to_string())); assert_eq!(
assert_eq!(metadata.tags.get("replaygain_track_gain"), Some(&"-9.4 dB".to_string())); metadata.tags.get("albumartist"),
assert_eq!(metadata.tags.get("composer"), Some(&"Chad Kroeger".to_string())); Some(&"Alex Goot".to_string())
assert_eq!(metadata.tags.get("label"), Some(&"mudhutdigital.com".to_string())); );
assert_eq!(metadata.tags.get("publisher"), Some(&"mudhutdigital.com".to_string())); assert_eq!(
metadata.tags.get("replaygain_track_gain"),
Some(&"-9.4 dB".to_string())
);
assert_eq!(
metadata.tags.get("composer"),
Some(&"Chad Kroeger".to_string())
);
assert_eq!(
metadata.tags.get("label"),
Some(&"mudhutdigital.com".to_string())
);
assert_eq!(
metadata.tags.get("publisher"),
Some(&"mudhutdigital.com".to_string())
);
assert_eq!(metadata.tags.get("bpm"), Some(&"146".to_string())); assert_eq!(metadata.tags.get("bpm"), Some(&"146".to_string()));
} }
@ -258,18 +343,29 @@ mod tests {
#[test] #[test]
fn test_build_the_query_command_with_custom_socket_and_no_pass() { fn test_build_the_query_command_with_custom_socket_and_no_pass() {
let command = build_query_command("cmus-remote", &Some("/tmp/cmus-socket".to_string()), &None); let command =
build_query_command("cmus-remote", &Some("/tmp/cmus-socket".to_string()), &None);
assert_eq!(command.get_program(), "cmus-remote"); assert_eq!(command.get_program(), "cmus-remote");
assert_eq!(command.get_args().collect::<Vec<_>>(), &["--server", "/tmp/cmus-socket", "-Q"]); assert_eq!(
command.get_args().collect::<Vec<_>>(),
&["--server", "/tmp/cmus-socket", "-Q"]
);
} }
#[test] #[test]
fn test_build_the_query_command_with_custom_socket_and_pass() { fn test_build_the_query_command_with_custom_socket_and_pass() {
let command = build_query_command("cmus-remote", &Some("/tmp/cmus-socket".to_string()), &Some("pass".to_string())); let command = build_query_command(
"cmus-remote",
&Some("/tmp/cmus-socket".to_string()),
&Some("pass".to_string()),
);
assert_eq!(command.get_program(), "cmus-remote"); assert_eq!(command.get_program(), "cmus-remote");
assert_eq!(command.get_args().collect::<Vec<_>>(), &["--server", "/tmp/cmus-socket", "--passwd", "pass", "-Q"]); assert_eq!(
command.get_args().collect::<Vec<_>>(),
&["--server", "/tmp/cmus-socket", "--passwd", "pass", "-Q"]
);
} }
#[test] #[test]
@ -277,6 +373,9 @@ mod tests {
let command = build_query_command("flatpak run io.github.cmus.cmus", &None, &None); let command = build_query_command("flatpak run io.github.cmus.cmus", &None, &None);
assert_eq!(command.get_program(), "flatpak"); assert_eq!(command.get_program(), "flatpak");
assert_eq!(command.get_args().collect::<Vec<_>>(), &["run", "io.github.cmus.cmus", "-Q"]); assert_eq!(
command.get_args().collect::<Vec<_>>(),
&["run", "io.github.cmus.cmus", "-Q"]
);
} }
} }

View file

@ -10,6 +10,11 @@ fn main() {
// Build the command, or use the default. (to speed up the main loop, because we don't need to build it every time) // Build the command, or use the default. (to speed up the main loop, because we don't need to build it every time)
let mut query_command = cmus::build_query_command( let mut query_command = cmus::build_query_command(
&args.cmus_remote_bin_path.unwrap_or("cmus-remote".to_string()).as_str(), &args
&args.cmus_socket_address, &args.cmus_socket_password); .cmus_remote_bin_path
.unwrap_or("cmus-remote".to_string())
.as_str(),
&args.cmus_socket_address,
&args.cmus_socket_password,
);
} }

View file

@ -1,10 +1,14 @@
use std::path::Path;
use crate::cmus; use crate::cmus;
use std::path::Path;
/// Search in the track directory for the cover image or the lyrics(depending on the `regx`). /// Search in the track directory for the cover image or the lyrics(depending on the `regx`).
/// If the cover image or the lyrics is not found, search in the parent directory, and so on, until the max depth is reached. /// If the cover image or the lyrics is not found, search in the parent directory, and so on, until the max depth is reached.
/// If the cover image or the lyrics is not found, return `None`. /// If the cover image or the lyrics is not found, return `None`.
pub fn search_for(search_directory: &str, max_depth: u8, regx: &[&str]) -> std::io::Result<Option<String>> { pub fn search_for(
search_directory: &str,
max_depth: u8,
regx: &[&str],
) -> std::io::Result<Option<String>> {
// Search in the track directory. // Search in the track directory.
for entry in std::fs::read_dir(search_directory)? { for entry in std::fs::read_dir(search_directory)? {
if let Ok(entry) = entry { if let Ok(entry) = entry {
@ -24,7 +28,8 @@ pub fn search_for(search_directory: &str, max_depth: u8, regx: &[&str]) -> std::
// If the max depth is reached, return `None`. // If the max depth is reached, return `None`.
if max_depth == 0 { if max_depth == 0 {
Ok(None) Ok(None)
} else { // If the max depth is not reached, search in the parent directory (recursively). } else {
// If the max depth is not reached, search in the parent directory (recursively).
let Some(parent) = Path::new(search_directory).parent() else { return Ok(None); }; let Some(parent) = Path::new(search_directory).parent() else { return Ok(None); };
let Some(parent) = parent.to_str() else { return Ok(None); }; let Some(parent) = parent.to_str() else { return Ok(None); };
search_for(parent, max_depth - 1, regx) search_for(parent, max_depth - 1, regx)
@ -41,11 +46,15 @@ pub fn process_template_placeholders(template: &String, track: &cmus::Track) ->
for c in template.chars() { for c in template.chars() {
if c == '{' { if c == '{' {
key = String::new(); key = String::new();
} else if c == '}' { // Replace the key with their matching value if exists, if not replace with the empty string. } else if c == '}' {
processed = processed.replace(&format!("{{{}}}", key), match key.as_str() { // Replace the key with their matching value if exists, if not replace with the empty string.
"title" => track.get_name(), processed = processed.replace(
_ => track.metadata.get(&key).unwrap_or(""), &format!("{{{}}}", key),
}); match key.as_str() {
"title" => track.get_name(),
_ => track.metadata.get(&key).unwrap_or(""),
},
);
} else { } else {
key.push(c); key.push(c);
} }
@ -67,7 +76,10 @@ mod tests {
impl TestContext for TestContextWithFullTrack { impl TestContext for TestContextWithFullTrack {
fn setup() -> Self { fn setup() -> Self {
Self { Self {
track: cmus::Track::from_str(include_str!("../tests/samples/cmus-remote-output-with-all-tags.txt")).unwrap() track: cmus::Track::from_str(include_str!(
"../tests/samples/cmus-remote-output-with-all-tags.txt"
))
.unwrap(),
} }
} }
} }
@ -78,6 +90,9 @@ mod tests {
let cover_path_template = String::from("{title}/{artist}/{album}/{tracknumber}"); let cover_path_template = String::from("{title}/{artist}/{album}/{tracknumber}");
let cover_path = process_template_placeholders(&cover_path_template, &ctx.track); let cover_path = process_template_placeholders(&cover_path_template, &ctx.track);
assert_eq!(cover_path, "Photograph/Alex Goot/Alex Goot & Friends, Vol. 3/8"); assert_eq!(
cover_path,
"Photograph/Alex Goot/Alex Goot & Friends, Vol. 3/8"
);
} }
} }