use std::{fs, io::{self, Cursor}, str::FromStr, time::Duration};
use base64::{prelude::BASE64_STANDARD, Engine};
use image::ImageReader;
use mpris::{Metadata, MetadataValue, PlaybackStatus, Player, PlayerFinder, TrackID};
use ratatui_image::{picker::Picker, protocol::StatefulProtocol};
use reqwest::{header::RANGE, Url};
use crate::{config::Config, fum::FumResult};
#[derive(Clone)]
pub struct CoverArt {
pub url: String,
pub image: StatefulProtocol
}
#[derive(Clone)]
pub struct Meta {
pub metadata: Metadata,
pub track_id: Option<TrackID>,
pub title: String,
pub artists: Vec<String>,
pub album: String,
pub status: PlaybackStatus,
pub status_icon: char,
pub status_text: String,
pub position: Duration,
pub length: Duration,
pub volume: f64,
pub cover_art: Option<CoverArt>,
pub changed: bool
}
impl Default for Meta {
fn default() -> Self {
Self {
metadata: Metadata::default(),
track_id: None,
title: "No Music".to_string(),
artists: vec!["Artist".to_string()],
album: "Album".to_string(),
status: PlaybackStatus::Stopped,
status_icon: Meta::get_status_icon(&PlaybackStatus::Stopped),
status_text: "stopped".to_string(),
position: Duration::from_secs(0),
length: Duration::from_secs(0),
volume: 0.0,
cover_art: None,
changed: false
}
}
}
impl Meta {
pub fn fetch(player: &Player, picker: &Picker, current: Option<&Self>) -> FumResult<Self> {
let metadata = Meta::get_metadata(player)?;
let track_id = Meta::get_trackid(&metadata).ok();
let title = Meta::get_title(&metadata)?;
let artists = Meta::get_artists(&metadata).unwrap_or(vec!["Artist".to_string()]);
let album = Meta::get_album(&metadata).unwrap_or("Album".to_string());
let status = Meta::get_status(player)?;
let status_icon = Meta::get_status_icon(&status);
let status_text = Meta::get_status_text(&status);
let position = Meta::get_position(player)?;
let length = Meta::get_length(&metadata)?;
let volume = Meta::get_volume(player).unwrap_or(0.0);
let cover_art = Meta::get_cover_art(&metadata, picker, current).ok();
let mut changed = false;
if let Some(current) = ¤t {
if current.title != title ||
current.artists != artists ||
current.status != status ||
current.length != length ||
current.volume != volume ||
position.as_secs() > current.position.as_secs() ||
position.as_secs() < current.position.as_secs() {
changed = true;
}
}
Ok(Self {
metadata,
track_id,
title,
artists,
album,
status,
status_icon,
status_text,
position,
length,
volume,
cover_art,
changed
})
}
pub fn get_player(config: &Config) -> FumResult<Player> {
let finder = PlayerFinder::new()
.map_err(|err| format!("Failed to connect to D-Bus: {:?}.", err))?;
let players = finder
.find_all()
.map_err(|err| format!("There is no any active players: {:?}.", err))?;
for player in players {
let identity = player.identity().to_lowercase();
let bus_name = player.bus_name().to_lowercase();
if config.players.iter().any(|p|
p.to_lowercase() == identity.to_lowercase() ||
bus_name.starts_with(&p.to_lowercase())
) {
return Ok(player);
}
}
if config.use_active_player {
let active = finder.find_active()
.map_err(|err| format!("'use-active-player' is set to true but failed to get active player: {err}"))?;
return Ok(active);
}
Err(Box::new(
io::Error::new(
io::ErrorKind::Other,
"Failed to find any specified players"
)
))
}
pub fn get_metadata(player: &Player) -> FumResult<Metadata> {
let metadata = player.get_metadata()?;
Ok(metadata)
}
pub fn get_trackid(metadata: &Metadata) -> FumResult<TrackID> {
let trackid = metadata.track_id()
.ok_or("Failed to get track_id")?;
Ok(trackid)
}
pub fn get_title(metadata: &Metadata) -> FumResult<String> {
let title = metadata
.title()
.map(|t| t.to_string())
.ok_or("Failed to get xesam:title")?;
Ok(title)
}
pub fn get_artists(metadata: &Metadata) -> FumResult<Vec<String>> {
let metadata = metadata
.artists()
.map(|a| a.iter().map(|a| a.to_string()).collect())
.ok_or("Failed to get xesam:title.".to_string())?;
Ok(metadata)
}
pub fn get_status(player: &Player) -> FumResult<PlaybackStatus> {
let status = player
.get_playback_status()
.map_err(|err| format!("Failed to get player playback_status: {err}"))?;
Ok(status)
}
pub fn get_status_icon(status: &PlaybackStatus) -> char {
match status {
PlaybackStatus::Stopped => '',
PlaybackStatus::Playing => '',
PlaybackStatus::Paused => ''
}
}
pub fn get_status_text(status: &PlaybackStatus) -> String {
match status {
PlaybackStatus::Stopped => "stopped".to_string(),
PlaybackStatus::Playing => "playing".to_string(),
PlaybackStatus::Paused => "paused".to_string()
}
}
pub fn get_position(player: &Player) -> FumResult<Duration> {
let position = player.get_position()
.map_err(|err| format!("Failed to get player position: {err}"))?;
Ok(position)
}
pub fn get_length(metadata: &Metadata) -> FumResult<Duration> {
let length = metadata
.length()
.ok_or("Failed to get mpris:length".to_string())?;
Ok(length)
}
pub fn get_album(metadata: &Metadata) -> FumResult<String> {
let album = metadata
.album_name()
.map(|a| a.to_string())
.ok_or("Failed to get xesam:album".to_string())?;
Ok(album)
}
pub fn get_volume(player: &Player) -> FumResult<f64> {
let volume = player.get_volume()
.map_err(|err| format!("Failed to get player volume: {err}"))?;
Ok(volume)
}
pub fn get_custom_meta(metadata: &Metadata, key: String) -> String {
let value = metadata.get(&key);
match value {
Some(value) => match value {
MetadataValue::String(str) => str.to_string(),
MetadataValue::Bool(bool) => bool.to_string(),
MetadataValue::U8(u8) => u8.to_string(),
MetadataValue::U16(u16) => u16.to_string(),
MetadataValue::U32(u32) => u32.to_string(),
MetadataValue::U64(u64) => u64.to_string(),
MetadataValue::I16(i16) => i16.to_string(),
MetadataValue::I32(i32) => i32.to_string(),
MetadataValue::I64(i64) => i64.to_string(),
MetadataValue::F64(f64) => f64.to_string(),
MetadataValue::Unsupported | _ => "!Unsupported".to_string()
},
None => "!NotFound".to_string()
}
}
pub fn get_cover_art(metadata: &Metadata, picker: &Picker, current: Option<&Meta>) -> FumResult<CoverArt> {
let art_url = metadata
.get("mpris:artUrl")
.ok_or("Failed to get mpris:artUrl")?;
if let mpris::MetadataValue::String(art_url) = art_url {
if let Some(current) = ¤t {
if let Some(current_art) = ¤t.cover_art {
if current_art.url == *art_url {
return Ok(current_art.clone());
}
}
}
if art_url.starts_with("file://") {
let art_path = Url::from_str(&art_url)
.map_err(|err| format!("Failed to parse url: {art_url}: {err}"))?
.to_file_path()
.map_err(|_| format!("Failed to convert url: {art_url} to file_path"))?;
let bytes = fs::read(&art_path)
.map_err(|err| format!("Failed to read art file: {err}"))?;
let cover_art = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|_| "Unknown image file_type".to_string())?
.decode()
.map_err(|_| "Failed to decode image".to_string())?;
return Ok(CoverArt {
url: art_url.to_string(),
image: picker.new_resize_protocol(cover_art)
})
}
if art_url.starts_with("data:") {
let base64_data = art_url
.split_once("base64,")
.ok_or("Invalid base64 url format")?
.1;
let bytes = BASE64_STANDARD.decode(base64_data)
.map_err(|err| format!("Failed to decode base64 data: {err}"))?;
let cover_art = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|_| "Unknown image file_type".to_string())?
.decode()
.map_err(|_| "Failed to decode image".to_string())?;
return Ok(CoverArt {
url: art_url.to_string(),
image: picker.new_resize_protocol(cover_art),
});
}
let client = reqwest::blocking::Client::new();
let resp = client
.get(art_url)
.header(RANGE, "bytes=0-1048576")
.send()
.map_err(|_| "Failed to fetch art url".to_string())?;
let bytes = resp.bytes()
.map_err(|_| "Failed to get art image bytes".to_string())?;
let cover_art = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|_| "Unknown image file_type".to_string())?
.decode()
.map_err(|_| "Failed to decode image".to_string())?;
return Ok(CoverArt {
url: art_url.to_string(),
image: picker.new_resize_protocol(cover_art)
})
}
Err(Box::new(
io::Error::new(
io::ErrorKind::Other,
"mpris:artUrl is not a string."
)
))
}
}