From d3ae9f809b975e3d887c0212a6f216636612daa2 Mon Sep 17 00:00:00 2001 From: Anas Elgarhy Date: Sat, 4 Feb 2023 22:12:09 +0200 Subject: [PATCH] Create the base cmus utils module --- Cargo.lock | 12 ++ Cargo.toml | 1 + src/cmus/mod.rs | 167 ++++++++++++++++++ src/main.rs | 3 + .../cmus-remote-output-with-all-tags.txt | 30 ++++ 5 files changed, 213 insertions(+) create mode 100644 src/cmus/mod.rs create mode 100644 tests/samples/cmus-remote-output-with-all-tags.txt diff --git a/Cargo.lock b/Cargo.lock index f422c45..9fba74a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,7 @@ dependencies = [ "clap", "lrc", "notify-rust", + "typed-builder", "walkdir", ] @@ -1530,6 +1531,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typed-builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6179333b981641242a768f30f371c9baccbfcc03749627000c500ab88bf4528b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.16.0" diff --git a/Cargo.toml b/Cargo.toml index d737a10..0c4a663 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] lrc = { version = "0.1.7", optional = true } notify-rust = { version = "4.7.0", features = ["images"] } +typed-builder = "0.12.0" walkdir = "2.3.2" [dependencies.clap] diff --git a/src/cmus/mod.rs b/src/cmus/mod.rs new file mode 100644 index 0000000..30a0717 --- /dev/null +++ b/src/cmus/mod.rs @@ -0,0 +1,167 @@ +use std::collections::HashMap; +use std::num::ParseIntError; +use std::str::FromStr; +use typed_builder::TypedBuilder; + +#[derive(Debug, PartialEq, TypedBuilder)] +pub struct TrackMetadata { + tags: HashMap, +} + +#[derive(Debug, PartialEq)] +pub enum TrackStatus { + Playing, + Paused, + Stopped, +} + +#[derive(Debug, TypedBuilder, PartialEq)] +pub struct Track { + pub status: TrackStatus, + pub path: String, + pub metadata: TrackMetadata, + pub duration: u32, + pub position: u32, +} + +impl FromStr for TrackStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "playing" => Ok(TrackStatus::Playing), + "paused" => Ok(TrackStatus::Paused), + "stopped" => Ok(TrackStatus::Stopped), + _ => Err(format!("Unknown status: {}", s)), + } + } +} + +impl FromStr for Track { + type Err = String; + + /// Creates a `Track` from the output of `cmus-remote -Q`. + /// + /// Pares the first 4 lines. + /// The first line is the status, the second is the path, the third is the duration, and the fourth is the position. + /// The rest of the lines are tags, and the player settings, so we'll send them to `TrackMetadata::parse`, to get the tags. + fn from_str(s: &str) -> Result { + let mut lines = s.lines(); + + Ok(Track::builder().status( + TrackStatus::from_str(lines.next().ok_or("Missing status")?.split_once(' ') + .ok_or("Unknown status")?.1)? + ) + .path(lines.next().ok_or("Missing path")?.split_once(' ') + .ok_or("Empty path")?.1.to_string()) + .duration( + lines.next().ok_or("Missing duration")?.split_once(' ') + .ok_or("Empty duration")?.1.parse().map_err(|e: ParseIntError| e.to_string())? + ) + .position( + lines.next().ok_or("Missing position")?.split_once(' ') + .ok_or("Empty position")?.1.parse().map_err(|e: ParseIntError| e.to_string())? + ) + .metadata(TrackMetadata::parse(lines)) + .build()) + } +} + +impl TrackMetadata { + /// Parse the tags from the rest of `cmus-remote -Q` output. + /// 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. + fn parse<'a>(mut lines: impl Iterator) -> Self { + let mut tags = HashMap::new(); + + while let Some(line) = lines.next() { + match line.trim().split_once(' ') { + Some(("tag", rest)) => { + let Some((key, value)) = rest.split_once(' ') else { + continue; // Ignore lines that don't have a key and a value. + }; + tags.insert(key.to_string(), value.to_string()); + } + _ => break, // We've reached the end of the tags. + } + } + + TrackMetadata { tags } + } + + fn get(&self, key: &str) -> Option<&str> { + self.tags.get(key).map(|s| s.as_str()) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::assert_matches::assert_matches; + + 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 + tag album Alex Goot & Friends, Vol. 3 + tag title Photograph + tag date 2014 + tag genre Pop + tag discnumber 1 + tag tracknumber 8 + tag albumartist Alex Goot + tag replaygain_track_gain -9.4 dB + tag composer Chad Kroeger + tag label mudhutdigital.com + tag publisher mudhutdigital.com + tag bpm 146 + set aaa_mode artist + ..."#; + + #[test] + fn test_create_track_from_str() { + let track = Track::from_str(OUTPUT_WITH_ALL_TAGS); + + assert_matches!(track, Ok(_)); + + let track = track.unwrap(); + + assert_eq!(track.status, TrackStatus::Playing); + 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.position, 226); + assert_eq!(track.metadata.tags.get("artist"), 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("genre"), Some(&"Pop".to_string())); + assert_eq!(track.metadata.tags.get("discnumber"), Some(&"1".to_string())); + assert_eq!(track.metadata.tags.get("tracknumber"), 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())); + } + + #[test] + fn test_parse_metadata_from_the_string() { + let metadata = TrackMetadata::parse(SOME_TAGS.lines()); + + 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("title"), Some(&"Photograph".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("discnumber"), Some(&"1".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!(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())); + } +} diff --git a/src/main.rs b/src/main.rs index 17c790f..336920e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ +#![feature(assert_matches)] + mod arguments; +mod cmus; use clap::Parser; diff --git a/tests/samples/cmus-remote-output-with-all-tags.txt b/tests/samples/cmus-remote-output-with-all-tags.txt new file mode 100644 index 0000000..42284d5 --- /dev/null +++ b/tests/samples/cmus-remote-output-with-all-tags.txt @@ -0,0 +1,30 @@ +status playing +file /mnt/Data/Music/FLAC/Alex Goot/Alex Goot - Alex Goot & Friends, Vol. 3/08 - Photograph.mp3 +duration 284 +position 226 +tag artist Alex Goot +tag album Alex Goot & Friends, Vol. 3 +tag title Photograph +tag date 2014 +tag genre Pop +tag discnumber 1 +tag tracknumber 8 +tag albumartist Alex Goot +tag replaygain_track_gain -9.4 dB +tag composer Chad Kroeger +tag label mudhutdigital.com +tag publisher mudhutdigital.com +tag bpm 146 +set aaa_mode artist +set continue true +set play_library true +set play_sorted true +set replaygain disabled +set replaygain_limit true +set replaygain_preamp 0.000000 +set repeat false +set repeat_current false +set shuffle tracks +set softvol false +set vol_left 46 +set vol_right 46