use regex::Regex;
use serde_json::{json, Map, Value};
use std::sync::OnceLock;
pub fn state_symbols() -> Map<String, Value> {
let mut m = Map::new();
m.insert("fallback".to_string(), Value::String("".into()));
m.insert("play".to_string(), Value::String(">".into()));
m.insert("pause".to_string(), Value::String("~".into()));
m.insert("stop".to_string(), Value::String("X".into()));
m
}
pub fn _convert_state(state: &str) -> &'static str {
let lower = state.to_lowercase();
if lower.contains("play") {
"play"
} else if lower.contains("pause") {
"pause"
} else if lower.contains("stop") {
"stop"
} else {
"fallback"
}
}
pub fn _convert_seconds(seconds: f64) -> String {
let s = seconds.max(0.0);
let mins = (s / 60.0).floor();
let secs = s - mins * 60.0;
format!("{:.0}:{:02.0}", mins, secs)
}
pub fn _convert_seconds_str(seconds: &str) -> Option<String> {
let normalized = seconds.replace(',', ".");
let parsed: f64 = normalized.trim().parse().ok()?;
Some(_convert_seconds(parsed))
}
#[derive(Debug, Clone, Default)]
pub struct PlayerStats {
pub state: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
pub title: Option<String>,
pub elapsed: Option<String>,
pub total: Option<String>,
}
impl PlayerStats {
pub fn fallback() -> Self {
Self {
state: Some("fallback".to_string()),
..Default::default()
}
}
}
pub fn player_segment_call(
func_stats: Option<PlayerStats>,
format: &str,
state_symbols_map: &Map<String, Value>,
) -> Option<Vec<Value>> {
let stats = func_stats?;
let state = stats
.state
.clone()
.unwrap_or_else(|| "fallback".to_string());
let state_symbol = state_symbols_map
.get(&state)
.and_then(|v| v.as_str())
.unwrap_or("");
let contents = format
.replace("{state_symbol}", state_symbol)
.replace("{album}", stats.album.as_deref().unwrap_or(""))
.replace("{artist}", stats.artist.as_deref().unwrap_or(""))
.replace("{title}", stats.title.as_deref().unwrap_or(""))
.replace("{elapsed}", stats.elapsed.as_deref().unwrap_or(""))
.replace("{total}", stats.total.as_deref().unwrap_or(""));
Some(vec![json!({
"contents": contents,
"highlight_groups": [format!("player_{}", state), "player"],
})])
}
pub fn get_player_status() -> Option<PlayerStats> {
None
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CmusPlayerSegment;
impl CmusPlayerSegment {
pub fn get_player_status(&self, now_playing_str: &str) -> Option<PlayerStats> {
if now_playing_str.is_empty() {
return None;
}
let ignore_levels = ["tag", "set"];
let mut now_playing: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for line in now_playing_str.split('\n') {
if line.is_empty() {
continue;
}
let tokens: Vec<&str> = line.split(' ').collect();
if tokens.is_empty() {
continue;
}
let (key, value) = if ignore_levels.contains(&tokens[0]) {
if tokens.len() < 2 {
continue;
}
(tokens[1].to_string(), tokens[2..].join(" "))
} else {
(tokens[0].to_string(), tokens[1..].join(" "))
};
now_playing.insert(key, value);
}
let state = _convert_state(now_playing.get("status").map(|s| s.as_str()).unwrap_or(""));
let parse_secs = |k: &str| {
now_playing
.get(k)
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
};
Some(PlayerStats {
state: Some(state.to_string()),
album: now_playing.get("album").cloned(),
artist: now_playing.get("artist").cloned(),
title: now_playing.get("title").cloned(),
elapsed: Some(_convert_seconds(parse_secs("position"))),
total: Some(_convert_seconds(parse_secs("duration"))),
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MpdPlayerSegment;
#[allow(non_snake_case)]
pub fn MPC_OUTPUT_RE() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| {
Regex::new(r"(?s)(.*) - (.*)\n\[([a-z]+)\] +[#0-9/]+ +([0-9:]+)/([0-9:]+)").unwrap()
})
}
impl MpdPlayerSegment {
pub fn get_player_status(&self, now_playing: &str, album: Option<&str>) -> Option<PlayerStats> {
if now_playing.is_empty() || now_playing.matches('\n').count() != 3 {
return None;
}
let caps = MPC_OUTPUT_RE().captures(now_playing)?;
let state = _convert_state(caps.get(3)?.as_str());
Some(PlayerStats {
state: Some(state.to_string()),
album: album.map(str::to_string),
artist: Some(caps.get(1)?.as_str().to_string()),
title: Some(caps.get(2)?.as_str().to_string()),
elapsed: Some(caps.get(4)?.as_str().to_string()),
total: Some(caps.get(5)?.as_str().to_string()),
})
}
}
#[allow(clippy::too_many_arguments)]
pub fn _get_dbus_player_status(
status: &str,
album: Option<&str>,
title: Option<&str>,
artist: Option<&str>,
elapsed_micros: Option<i64>,
length_micros: Option<i64>,
) -> Option<PlayerStats> {
let elapsed = elapsed_micros.map(|m| _convert_seconds((m as f64) / 1_000_000.0));
let state = _convert_state(status);
let total = length_micros.map(|m| _convert_seconds((m as f64) / 1_000_000.0));
Some(PlayerStats {
state: Some(state.to_string()),
album: album.map(str::to_string),
title: title.map(str::to_string),
artist: artist.map(str::to_string),
elapsed,
total,
})
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DbusPlayerSegment;
#[derive(Debug, Clone, Copy, Default)]
pub struct SpotifyDbusPlayerSegment;
#[derive(Debug, Clone, Copy, Default)]
pub struct SpotifyAppleScriptPlayerSegment;
pub const APPLESCRIPT_STATUS_DELIMITER: &str = "-~`/=";
impl SpotifyAppleScriptPlayerSegment {
pub fn get_player_status(&self, spotify: &str) -> Option<PlayerStats> {
if spotify.is_empty() {
return None;
}
let parts: Vec<&str> = spotify.split(APPLESCRIPT_STATUS_DELIMITER).collect();
if parts.len() < 6 {
return None;
}
let state = _convert_state(parts[0]);
if state == "stop" {
return None;
}
let total_ms: f64 = parts[4].trim().parse().ok()?;
let total = _convert_seconds(total_ms / 1000.0);
let elapsed_secs: f64 = parts[5].trim().parse().ok()?;
let elapsed = _convert_seconds(elapsed_secs);
Some(PlayerStats {
state: Some(state.to_string()),
album: Some(parts[1].to_string()),
artist: Some(parts[2].to_string()),
title: Some(parts[3].to_string()),
elapsed: Some(elapsed),
total: Some(total),
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ClementinePlayerSegment;
#[derive(Debug, Clone, Copy, Default)]
pub struct RhythmboxPlayerSegment;
impl RhythmboxPlayerSegment {
pub fn get_player_status(&self, now_playing: &str) -> Option<PlayerStats> {
if now_playing.is_empty() {
return None;
}
let parts: Vec<&str> = now_playing.split('\n').collect();
if parts.len() < 5 {
return None;
}
Some(PlayerStats {
state: None,
album: Some(parts[0].to_string()),
artist: Some(parts[1].to_string()),
title: Some(parts[2].to_string()),
elapsed: Some(parts[3].to_string()),
total: Some(parts[4].to_string()),
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RDIOPlayerSegment;
impl RDIOPlayerSegment {
pub fn get_player_status(&self, now_playing: &str) -> Option<PlayerStats> {
if now_playing.is_empty() {
return None;
}
let parts: Vec<&str> = now_playing.split(APPLESCRIPT_STATUS_DELIMITER).collect();
if parts.len() != 6 {
return None;
}
let state = _convert_state(parts[5]);
let total_secs: f64 = parts[4].trim().parse().ok()?;
let total = _convert_seconds(total_secs);
let elapsed_pct: f64 = parts[3].trim().parse().ok()?;
let elapsed = _convert_seconds(elapsed_pct * total_secs / 100.0);
Some(PlayerStats {
state: Some(state.to_string()),
title: Some(parts[0].to_string()),
artist: Some(parts[1].to_string()),
album: Some(parts[2].to_string()),
elapsed: Some(elapsed),
total: Some(total),
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ITunesPlayerSegment;
impl ITunesPlayerSegment {
pub fn get_player_status(&self, now_playing: &str) -> Option<PlayerStats> {
if now_playing.is_empty() {
return None;
}
let parts: Vec<&str> = now_playing.split(APPLESCRIPT_STATUS_DELIMITER).collect();
if parts.len() != 6 {
return None;
}
let state = _convert_state(parts[5]);
let total_secs: f64 = parts[4].trim().parse().ok()?;
let elapsed_secs: f64 = parts[3].trim().parse().ok()?;
Some(PlayerStats {
state: Some(state.to_string()),
title: Some(parts[0].to_string()),
artist: Some(parts[1].to_string()),
album: Some(parts[2].to_string()),
elapsed: Some(_convert_seconds(elapsed_secs)),
total: Some(_convert_seconds(total_secs)),
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MocPlayerSegment;
impl MocPlayerSegment {
pub fn get_player_status(&self, now_playing_str: &str) -> Option<PlayerStats> {
if now_playing_str.is_empty() {
return None;
}
let mut now_playing: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for line in now_playing_str.split('\n') {
if line.is_empty() {
continue;
}
if let Some((k, v)) = line.split_once(": ") {
now_playing.insert(k.to_string(), v.to_string());
}
}
let state = _convert_state(
now_playing
.get("State")
.map(|s| s.as_str())
.unwrap_or("stop"),
);
let parse_secs = |k: &str| {
now_playing
.get(k)
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
};
Some(PlayerStats {
state: Some(state.to_string()),
album: Some(now_playing.get("Album").cloned().unwrap_or_default()),
artist: Some(now_playing.get("Artist").cloned().unwrap_or_default()),
title: Some(now_playing.get("SongTitle").cloned().unwrap_or_default()),
elapsed: Some(_convert_seconds(parse_secs("CurrentSec"))),
total: Some(_convert_seconds(parse_secs("TotalSec"))),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_symbols_table_matches_upstream() {
let s = state_symbols();
assert_eq!(s.get("fallback"), Some(&Value::String("".into())));
assert_eq!(s.get("play"), Some(&Value::String(">".into())));
assert_eq!(s.get("pause"), Some(&Value::String("~".into())));
assert_eq!(s.get("stop"), Some(&Value::String("X".into())));
}
#[test]
fn convert_state_play_returns_play() {
assert_eq!(_convert_state("playing"), "play");
assert_eq!(_convert_state("Play"), "play");
assert_eq!(_convert_state("PLAY"), "play");
}
#[test]
fn convert_state_pause_returns_pause() {
assert_eq!(_convert_state("paused"), "pause");
assert_eq!(_convert_state("Pause"), "pause");
}
#[test]
fn convert_state_stop_returns_stop() {
assert_eq!(_convert_state("stopped"), "stop");
assert_eq!(_convert_state("STOPPED"), "stop");
}
#[test]
fn convert_state_unknown_returns_fallback() {
assert_eq!(_convert_state("loading"), "fallback");
assert_eq!(_convert_state(""), "fallback");
assert_eq!(_convert_state("buffering"), "fallback");
}
#[test]
fn convert_state_play_takes_precedence_over_pause() {
assert_eq!(_convert_state("playpause"), "play");
}
#[test]
fn convert_seconds_zero_emits_zero_zero_zero() {
assert_eq!(_convert_seconds(0.0), "0:00");
}
#[test]
fn convert_seconds_under_a_minute_pads_to_two_digits() {
assert_eq!(_convert_seconds(45.0), "0:45");
}
#[test]
fn convert_seconds_one_minute_emits_one_zero_zero() {
assert_eq!(_convert_seconds(60.0), "1:00");
}
#[test]
fn convert_seconds_multi_minute_pads_seconds() {
assert_eq!(_convert_seconds(125.0), "2:05");
}
#[test]
fn convert_seconds_handles_large_values() {
assert_eq!(_convert_seconds(3661.0), "61:01");
}
#[test]
fn convert_seconds_str_accepts_dot_notation() {
assert_eq!(_convert_seconds_str("60.5"), Some("1:00".to_string()));
}
#[test]
fn convert_seconds_str_accepts_comma_notation() {
assert_eq!(_convert_seconds_str("60,5"), Some("1:00".to_string()));
}
#[test]
fn convert_seconds_str_invalid_returns_none() {
assert!(_convert_seconds_str("not a number").is_none());
}
#[test]
fn player_stats_fallback_initial_state() {
let s = PlayerStats::fallback();
assert_eq!(s.state.as_deref(), Some("fallback"));
assert!(s.album.is_none());
assert!(s.artist.is_none());
assert!(s.title.is_none());
assert!(s.elapsed.is_none());
assert!(s.total.is_none());
}
#[test]
fn player_segment_call_no_stats_returns_none() {
let symbols = state_symbols();
let r = player_segment_call(None, "{state_symbol}", &symbols);
assert!(r.is_none());
}
#[test]
fn player_segment_call_emits_player_state_highlight_group() {
let symbols = state_symbols();
let stats = PlayerStats {
state: Some("play".to_string()),
artist: Some("Pink Floyd".to_string()),
title: Some("Time".to_string()),
..Default::default()
};
let r = player_segment_call(Some(stats), "{state_symbol} {artist} - {title}", &symbols)
.unwrap();
assert_eq!(r[0]["highlight_groups"][0], "player_play");
assert_eq!(r[0]["highlight_groups"][1], "player");
assert_eq!(r[0]["contents"], "> Pink Floyd - Time");
}
#[test]
fn player_segment_call_substitutes_all_placeholders() {
let symbols = state_symbols();
let stats = PlayerStats {
state: Some("play".to_string()),
album: Some("The Wall".to_string()),
artist: Some("Pink Floyd".to_string()),
title: Some("Time".to_string()),
elapsed: Some("1:23".to_string()),
total: Some("4:56".to_string()),
};
let r = player_segment_call(
Some(stats),
"{state_symbol}|{album}|{artist}|{title}|{elapsed}|{total}",
&symbols,
)
.unwrap();
assert_eq!(r[0]["contents"], ">|The Wall|Pink Floyd|Time|1:23|4:56");
}
#[test]
fn player_segment_call_empty_fields_become_empty_strings() {
let symbols = state_symbols();
let stats = PlayerStats {
state: Some("stop".to_string()),
..Default::default()
};
let r = player_segment_call(Some(stats), "{artist} - {title}", &symbols).unwrap();
assert_eq!(r[0]["contents"], " - ");
}
#[test]
fn player_segment_call_unknown_state_uses_fallback_symbol() {
let mut symbols = state_symbols();
symbols.remove("fallback");
let stats = PlayerStats {
state: Some("fallback".to_string()),
..Default::default()
};
let r = player_segment_call(Some(stats), "{state_symbol}|x", &symbols).unwrap();
assert_eq!(r[0]["contents"], "|x");
}
#[test]
fn player_segment_call_state_none_falls_back() {
let symbols = state_symbols();
let stats = PlayerStats {
state: None,
..Default::default()
};
let r = player_segment_call(Some(stats), "{state_symbol}", &symbols).unwrap();
assert_eq!(r[0]["contents"], "");
assert_eq!(r[0]["highlight_groups"][0], "player_fallback");
}
#[test]
fn player_segment_call_with_custom_state_symbols() {
let mut custom = state_symbols();
custom.insert("play".to_string(), Value::String("▶".into()));
let stats = PlayerStats {
state: Some("play".to_string()),
..Default::default()
};
let r = player_segment_call(Some(stats), "{state_symbol}", &custom).unwrap();
assert_eq!(r[0]["contents"], "▶");
}
#[test]
fn cmus_parses_basic_status() {
let raw = concat!(
"status playing\n",
"file /home/user/song.mp3\n",
"tag artist The Artist\n",
"tag title The Title\n",
"tag album The Album\n",
"set continue true\n",
"duration 245\n",
"position 30\n",
);
let s = CmusPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.artist.as_deref(), Some("The Artist"));
assert_eq!(s.title.as_deref(), Some("The Title"));
assert_eq!(s.album.as_deref(), Some("The Album"));
assert_eq!(s.elapsed.as_deref(), Some("0:30"));
assert_eq!(s.total.as_deref(), Some("4:05"));
}
#[test]
fn cmus_returns_none_for_empty_input() {
assert!(CmusPlayerSegment.get_player_status("").is_none());
}
#[test]
fn cmus_handles_paused_state() {
let raw = "status paused\ntag artist X\nposition 10\nduration 100\n";
let s = CmusPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("pause"));
}
#[test]
fn cmus_handles_set_level_keys() {
let raw = "status stopped\nset shuffle true\n";
let s = CmusPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("stop"));
}
#[test]
fn mpd_parses_mpc_output() {
let raw = "The Artist - The Title\n[playing] #1/10 0:30/4:05\nrandom: off repeat: on\n";
let s = MpdPlayerSegment
.get_player_status(raw, Some("The Album"))
.unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.artist.as_deref(), Some("The Artist"));
assert_eq!(s.title.as_deref(), Some("The Title"));
assert_eq!(s.elapsed.as_deref(), Some("0:30"));
assert_eq!(s.total.as_deref(), Some("4:05"));
assert_eq!(s.album.as_deref(), Some("The Album"));
}
#[test]
fn mpd_returns_none_when_wrong_newline_count() {
let raw = "Artist - Title\nstuff";
assert!(MpdPlayerSegment.get_player_status(raw, None).is_none());
}
#[test]
fn mpd_returns_none_when_empty() {
assert!(MpdPlayerSegment.get_player_status("", None).is_none());
}
#[test]
fn dbus_player_status_converts_micros_to_mss() {
let s = _get_dbus_player_status(
"Playing",
Some("Album"),
Some("Title"),
Some("Artist"),
Some(60_000_000), Some(245_000_000), )
.unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.elapsed.as_deref(), Some("1:00"));
assert_eq!(s.total.as_deref(), Some("4:05"));
assert_eq!(s.album.as_deref(), Some("Album"));
assert_eq!(s.title.as_deref(), Some("Title"));
assert_eq!(s.artist.as_deref(), Some("Artist"));
}
#[test]
fn dbus_player_status_none_elapsed_and_length() {
let s = _get_dbus_player_status("Paused", None, None, None, None, None).unwrap();
assert_eq!(s.state.as_deref(), Some("pause"));
assert!(s.elapsed.is_none());
assert!(s.total.is_none());
}
#[test]
fn applescript_delimiter_matches_python() {
assert_eq!(APPLESCRIPT_STATUS_DELIMITER, "-~`/=");
}
#[test]
fn spotify_applescript_parses_playing() {
let raw = "playing-~`/=The Album-~`/=The Artist-~`/=The Track-~`/=180000-~`/=45.5";
let s = SpotifyAppleScriptPlayerSegment
.get_player_status(raw)
.unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.album.as_deref(), Some("The Album"));
assert_eq!(s.artist.as_deref(), Some("The Artist"));
assert_eq!(s.title.as_deref(), Some("The Track"));
assert_eq!(s.total.as_deref(), Some("3:00"));
assert_eq!(s.elapsed.as_deref(), Some("0:46"));
}
#[test]
fn spotify_applescript_returns_none_for_stop() {
let raw = "stopped-~`/=-~`/=-~`/=-~`/=0-~`/=0";
assert!(SpotifyAppleScriptPlayerSegment
.get_player_status(raw)
.is_none());
}
#[test]
fn spotify_applescript_returns_none_for_empty() {
assert!(SpotifyAppleScriptPlayerSegment
.get_player_status("")
.is_none());
}
#[test]
fn rhythmbox_parses_5_field_output() {
let raw = "Album X\nArtist Y\nTitle Z\n0:30\n4:05";
let s = RhythmboxPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.album.as_deref(), Some("Album X"));
assert_eq!(s.artist.as_deref(), Some("Artist Y"));
assert_eq!(s.title.as_deref(), Some("Title Z"));
assert_eq!(s.elapsed.as_deref(), Some("0:30"));
assert_eq!(s.total.as_deref(), Some("4:05"));
assert!(s.state.is_none());
}
#[test]
fn rhythmbox_returns_none_for_empty() {
assert!(RhythmboxPlayerSegment.get_player_status("").is_none());
}
#[test]
fn rdio_parses_6_field_output_with_elapsed_pct() {
let raw = "Title-~`/=Artist-~`/=Album-~`/=50-~`/=200-~`/=playing";
let s = RDIOPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.title.as_deref(), Some("Title"));
assert_eq!(s.artist.as_deref(), Some("Artist"));
assert_eq!(s.album.as_deref(), Some("Album"));
assert_eq!(s.total.as_deref(), Some("3:20"));
assert_eq!(s.elapsed.as_deref(), Some("1:40"));
}
#[test]
fn rdio_returns_none_for_wrong_field_count() {
assert!(RDIOPlayerSegment
.get_player_status("only-~`/=three-~`/=fields")
.is_none());
}
#[test]
fn itunes_parses_6_field_output() {
let raw = "Title-~`/=Artist-~`/=Album-~`/=30-~`/=245-~`/=playing";
let s = ITunesPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.title.as_deref(), Some("Title"));
assert_eq!(s.artist.as_deref(), Some("Artist"));
assert_eq!(s.album.as_deref(), Some("Album"));
assert_eq!(s.elapsed.as_deref(), Some("0:30"));
assert_eq!(s.total.as_deref(), Some("4:05"));
}
#[test]
fn itunes_returns_none_for_wrong_field_count() {
assert!(ITunesPlayerSegment.get_player_status("x").is_none());
}
#[test]
fn mocp_parses_key_value_output() {
let raw = concat!(
"State: PLAY\n",
"File: song.mp3\n",
"Title: full title\n",
"Artist: The Artist\n",
"SongTitle: The Track\n",
"Album: The Album\n",
"TotalSec: 245\n",
"CurrentSec: 30\n",
);
let s = MocPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("play"));
assert_eq!(s.artist.as_deref(), Some("The Artist"));
assert_eq!(s.title.as_deref(), Some("The Track"));
assert_eq!(s.album.as_deref(), Some("The Album"));
assert_eq!(s.elapsed.as_deref(), Some("0:30"));
assert_eq!(s.total.as_deref(), Some("4:05"));
}
#[test]
fn mocp_defaults_state_to_stop_when_missing() {
let raw = "File: x.mp3\nTitle: x\n";
let s = MocPlayerSegment.get_player_status(raw).unwrap();
assert_eq!(s.state.as_deref(), Some("stop"));
}
#[test]
fn mocp_returns_none_for_empty() {
assert!(MocPlayerSegment.get_player_status("").is_none());
}
#[test]
fn mpc_output_re_captures_5_groups() {
let re = MPC_OUTPUT_RE();
let s = "Artist - Title\n[playing] #1/10 0:30/4:05";
let caps = re.captures(s).unwrap();
assert_eq!(caps.get(1).unwrap().as_str(), "Artist");
assert_eq!(caps.get(2).unwrap().as_str(), "Title");
assert_eq!(caps.get(3).unwrap().as_str(), "playing");
assert_eq!(caps.get(4).unwrap().as_str(), "0:30");
assert_eq!(caps.get(5).unwrap().as_str(), "4:05");
}
}