Added cover art, buffering, config, string truncating

This commit is contained in:
Ryze 2023-02-06 23:27:40 +03:00
parent 0cce7cbcd9
commit b87a0cf5d9
10 changed files with 320 additions and 64 deletions

View file

@ -10,6 +10,9 @@ crate-type = ["cdylib"]
[dependencies]
mpv-client = "0.4.1"
discord-rich-presence="0.2.3"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.92"
musicbrainz_rs = { version = "0.5.0", default-features = false, features = ["blocking"] }
[profile.release-full]
inherits = "release"

76
src/config.rs Normal file
View file

@ -0,0 +1,76 @@
use std::env;
use std::fs;
use serde::{self, Serialize, Deserialize};
use crate::logging::{self, Logger};
enum ConfigError {
CannotLoad,
ParseError(serde_json::Error)
}
#[derive(Serialize, Deserialize)]
pub struct Config {
#[serde(default = "active_default")]
pub active: bool,
#[serde(default = "cover_art_default")]
pub cover_art: bool
}
const fn active_default() -> bool {
false
}
const fn cover_art_default() -> bool {
true
}
impl Config {
pub fn default() -> Self {
Self {
active: active_default(),
cover_art: cover_art_default()
}
}
pub fn from_config_file(logger: &Logger) -> Self {
let path = Config::get_config_path();
logging::info!(logger, "Config path {path}");
match Config::parse_config_from_file(&path) {
Ok(config) => config,
Err(ConfigError::CannotLoad) => {
logging::info!(logger, "Cannot load config. Using default options");
Config::default()
}
Err(ConfigError::ParseError(e)) => {
logging::error!(logger, "Cannot parse config: {e}");
Config::default()
}
}
}
fn parse_config_from_file(path: &str) -> Result<Self, ConfigError> {
match fs::read_to_string(path) {
Ok(json) => Config::parse_config(&json),
Err(_) => Err(ConfigError::CannotLoad)
}
}
fn parse_config(json: &str) -> Result<Self, ConfigError>{
match serde_json::from_str(json) {
Ok(config) => Ok(config),
Err(e) => Err(ConfigError::ParseError(e))
}
}
fn get_config_path() -> String {
let mpv_home = env::var("MPV_HOME").or(
env::var("HOME").and_then(|home| Ok(home + "/.config/mpv/")).or(
env::var("XDG_CONFIG_HOME").and_then(|home| Ok(home + "/.mpv/"))
)
).unwrap_or("/etc/mpv/".to_string());
return mpv_home + "rpc.json"
}
}

View file

@ -1,21 +1,56 @@
use std::rc::Rc;
use std::time::SystemTime;
use crate::logging::{self, Logger};
use crate::mpv_event_queue::events::{MpvEventHandler, MpvEvent, FileInfo};
use std::collections::VecDeque;
use discord_rich_presence::{DiscordIpcClient, DiscordIpc};
use discord_rich_presence::activity::{Activity, Assets, Timestamps};
use crate::utils;
use crate::logging::{self, Logger};
use crate::mpv_event_queue::events::{MpvEventHandler, MpvEvent, FileInfo, MpvRequester, MpvRequest, FileMetadata};
const MAX_STR_LEN: usize = 128;
mod music_brainz;
struct ActivityInfo {
details: String,
state: String,
assets: AssetsInfo,
timestamps: Timestamps
}
impl AssetsInfo {
pub fn new(large_image: String, large_text: String) -> Self {
Self {
large_image,
large_text
}
}
pub fn empty() -> Self {
Self {
large_image: String::new(),
large_text: String::new(),
}
}
pub fn get_assets(&self) -> Assets {
Assets::new()
.large_image(&self.large_image)
.large_text(&self.large_text)
}
}
struct AssetsInfo {
large_image: String,
large_text: String
}
impl ActivityInfo {
pub fn new(details: String, state: String, timestamps: Timestamps) -> Self {
pub fn new(details: String, state: String, assets: AssetsInfo, timestamps: Timestamps) -> Self {
Self {
details,
state,
assets,
timestamps
}
}
@ -24,15 +59,14 @@ impl ActivityInfo {
Self {
details: String::new(),
state: String::new(),
assets: AssetsInfo::empty(),
timestamps: Timestamps::new()
}
}
pub fn get_activity(&self) -> Activity {
let assets = Assets::new()
.large_image("logo")
.large_text("mpv");
let assets = self.assets.get_assets();
Activity::new()
.assets(assets)
@ -43,26 +77,36 @@ impl ActivityInfo {
}
pub struct DiscordClient {
logger: Rc<Logger>,
discord: DiscordIpcClient,
activity_info: ActivityInfo,
active: bool
active: bool,
cover_art: bool,
mpv_requests: VecDeque<MpvRequest>,
logger: Rc<Logger>
}
impl DiscordClient {
pub fn new(client_id: &str, logger: Rc<Logger>) -> Result<Self, &'static str> {
pub fn new(client_id: &str, active: bool, cover_art: bool, logger: Rc<Logger>) -> Result<Self, &'static str> {
let discord = match DiscordIpcClient::new(client_id) {
Ok(discord) => discord,
Err(_) => return Err("cannot init discord client")
};
Ok(Self {
logger,
let mut new_self = Self {
discord,
activity_info: ActivityInfo::empty(),
active: false
})
active: false,
cover_art,
mpv_requests: VecDeque::new(),
logger
};
if active {
new_self.open()?;
}
Ok(new_self)
}
fn get_state(file_info: &FileInfo) -> String {
@ -76,6 +120,7 @@ impl DiscordClient {
if let Some(album) = &metadata.album {
state += &format!(" on {album}");
}
utils::truncate_string_fmt(&mut state, MAX_STR_LEN);
state
}
@ -86,12 +131,38 @@ impl DiscordClient {
None => return file_info.filename.clone()
};
if let Some(track) = &metadata.track {
let mut details = if let Some(track) = &metadata.track {
format!("{title} [T{track}] ")
}
else {
title.clone()
};
utils::truncate_string_fmt(&mut details, MAX_STR_LEN);
details
}
fn get_assets_info(cover_art: bool, metadata: FileMetadata) -> AssetsInfo {
let (large_image, large_text) = DiscordClient::get_large_info(cover_art, metadata);
AssetsInfo::new(large_image, large_text)
}
fn get_large_info(cover_art: bool, metadata: FileMetadata) -> (String, String) {
if !cover_art {
return ("logo".to_string(), "mpv".to_string())
}
let cover_art_url = music_brainz::get_cover_art_url(&metadata.title, &metadata.album, &metadata.artist);
let large_image = match cover_art_url {
Some(url) => url,
None => "logo".to_string()
};
let large_text = match metadata.title.or(metadata.album) {
Some(text) => text,
None => "mpv".to_string()
};
(large_image, large_text)
}
fn update_presence(&mut self) -> Result<(), &'static str> {
@ -105,19 +176,23 @@ impl DiscordClient {
Ok(()) => {
Ok(())
}
Err(_) => Err("cannot set presence")
Err(_) => {
self.active = false;
Err("cannot set presence")
}
}
}
fn set_presence(&mut self, file_info: FileInfo) -> Result<(), &'static str> {
let details = DiscordClient::get_details(&file_info);
let state = DiscordClient::get_state(&file_info);
self.activity_info = ActivityInfo::new(details, state, Timestamps::new());
let assets_info = DiscordClient::get_assets_info(self.cover_art, file_info.metadata);
self.activity_info = ActivityInfo::new(details, state, assets_info, Timestamps::new());
self.update_presence()
}
fn update_timestamps(&mut self, remaining_time: i64) -> Result<(), &'static str> {
fn set_timestamps(&mut self, remaining_time: i64) -> Result<(), &'static str> {
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH);
let current_time = match current_time {
Ok(time) => time.as_secs() as i64,
@ -130,6 +205,11 @@ impl DiscordClient {
self.update_presence()
}
fn clear_timestamps(&mut self) -> Result<(), &'static str> {
self.activity_info.timestamps = Timestamps::new();
self.update_presence()
}
fn open(&mut self) -> Result<(), &'static str> {
if self.active {
return Ok(());
@ -139,6 +219,7 @@ impl DiscordClient {
match self.discord.connect() {
Ok(()) => {
self.active = true;
self.request_osd_message("Discord RPC started");
self.update_presence()
}
Err(_) => Err("cannot connect to Discord")
@ -154,6 +235,7 @@ impl DiscordClient {
match self.discord.close() {
Ok(()) => {
self.active = false;
self.request_osd_message("Discord RPC stopped");
Ok(())
}
Err(_) => Err("cannot disconnect from Discord")
@ -166,16 +248,28 @@ impl DiscordClient {
true => self.close()
}
}
fn request_osd_message(&mut self, message: &'static str) {
self.mpv_requests.push_front(MpvRequest::OSDMessage(message));
}
}
impl MpvEventHandler for DiscordClient {
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str> {
match event {
MpvEvent::FileLoaded(file_info) => self.set_presence(file_info),
MpvEvent::Seek(remaining_time) => self.update_timestamps(remaining_time),
MpvEvent::Seek(remaining_time) => self.set_timestamps(remaining_time),
MpvEvent::Play(remaining_time) => self.set_timestamps(remaining_time),
MpvEvent::Pause(_) => self.clear_timestamps(),
MpvEvent::Buffering => self.clear_timestamps(),
MpvEvent::Toggle => self.toggle_activity(),
MpvEvent::Exit => self.close(),
_ => Ok(())
}
}
}
impl MpvRequester for DiscordClient {
fn next_request<'a>(&mut self) -> Option<MpvRequest> {
self.mpv_requests.pop_back()
}
}

View file

@ -0,0 +1,39 @@
use musicbrainz_rs::entity::release::{Release, ReleaseSearchQuery};
use musicbrainz_rs::entity::CoverartResponse;
use musicbrainz_rs::{Search, FetchCoverart};
pub fn get_cover_art_url(title: &Option<String>, album: &Option<String>, artist: &Option<String>) -> Option<String>{
let mut builder = ReleaseSearchQuery::query_builder();
if let Some(ref title) = title {
builder.release(title);
}
if let Some(ref album) = album {
builder.or().release(album);
}
if let Some(ref artist) = artist {
builder.and().artist(artist);
}
let query = builder.build();
let result = match Release::search(query).execute() {
Ok(res) => res,
Err(_) => return None
};
let release = match result.entities.get(0) {
Some(group) => group,
None => return None
};
let cover_art = match release.get_coverart().front().execute() {
Ok(art) => art,
Err(_) => return None
};
match cover_art {
CoverartResponse::Url(url) => Some(url),
_ => None
}
}

View file

@ -1,9 +1,11 @@
use mpv_client::mpv_handle;
mod config;
mod logging;
mod mpv_event_queue;
mod discord_client;
mod plugin;
mod utils;
use plugin::RPCPlugin;
@ -13,7 +15,10 @@ const DISCORD_APPID: &str = "1071519995588264016";
fn mpv_open_cplugin(handle: *mut mpv_handle) -> std::os::raw::c_int {
let plugin = match RPCPlugin::new(handle, DISCORD_APPID) {
Ok(plugin) => plugin,
Err(_) => return -1
Err(e) => {
println!("Error creating RPC plugin: {e}");
return -1;
}
};
plugin.run();

View file

@ -19,12 +19,11 @@ impl From<u32> for LogLevel {
1 => LogLevel::Error,
2 => LogLevel::Warn,
3 => LogLevel::Info,
_ => LogLevel::Error
_ => LogLevel::Info
}
}
}
#[allow(dead_code)]
pub struct Logger {
log_level: LogLevel
@ -65,5 +64,4 @@ impl Logger {
println!("[mpv-rpc (ERROR)] {}", message);
}
}
}

View file

@ -1,9 +1,9 @@
use std::rc::Rc;
use std::{rc::Rc, time::Duration};
use mpv_client::{Handle, Event, Property, Format, mpv_handle, ClientMessage};
use crate::logging::{self, Logger};
pub mod events;
use events::{MpvEvent, FileInfo, FileMetadata};
use events::{MpvEvent, MpvRequest, FileInfo, FileMetadata};
const NAME_PAUSE_PROP: &str = "pause";
@ -11,7 +11,7 @@ const REPL_PAUSE_PROP: u64 = 1;
pub struct MpvEventQueue {
mpv: Handle,
logger: Rc<Logger>,
logger: Rc<Logger>
}
impl MpvEventQueue {
@ -40,18 +40,31 @@ impl MpvEventQueue {
}
}
pub fn next_event(&self) -> Option<MpvEvent> {
pub fn next_event(&mut self) -> Option<MpvEvent> {
let event = self.mpv.wait_event(0.0);
let mpv_event = self.convert_event(event);
if let Some(ref event) = mpv_event {
logging::info!(self.logger, "Event: {event}");
}
mpv_event
}
pub fn handle_request(&self, request: MpvRequest) -> Result<(), &'static str> {
match request {
MpvRequest::OSDMessage(message) => self.display_osd_message(message)
}
}
pub fn display_osd_message(&self, message: &str) -> Result<(), &'static str> {
match self.mpv.osd_message(message, Duration::from_secs(1)) {
Ok(()) => Ok(()),
Err(_) => Err("cannot print OSD message")
}
}
fn convert_event(&self, event: Event) -> Option<MpvEvent> {
match event {
Event::None => (),
ref event => logging::info!(self.logger, "Event: {event}")
}
match event {
Event::FileLoaded => self.get_file_info_event(),
Event::PlaybackRestart => self.get_seek_event(),
@ -87,27 +100,33 @@ impl MpvEventQueue {
fn get_property_event(&self, prop_id: u64, prop: Property) -> Option<MpvEvent> {
logging::info!(self.logger, "Property changed: {prop_id}");
match prop_id {
1 => Some(self.convert_pause_prop(prop.data().unwrap())),
1 => self.convert_pause_prop(prop.data().unwrap()),
2 => self.convert_buffering_prop(prop.data().unwrap()),
_ => None
}
}
fn convert_pause_prop(&self, pause: bool) -> MpvEvent {
fn convert_pause_prop(&self, pause: bool) -> Option<MpvEvent> {
let time = self.get_remaining_time();
match pause {
false => MpvEvent::Play,
true => MpvEvent::Pause
false => Some(MpvEvent::Play(time)),
true => Some(MpvEvent::Pause(time))
}
}
pub fn convert_buffering_prop(&self, buffering: bool) -> Option<MpvEvent> {
match buffering {
true => Some(MpvEvent::Buffering),
false => None
}
}
fn get_seek_event(&self) -> Option<MpvEvent> {
let remaining_time = self.mpv.get_property("time-remaining").unwrap_or_else(|_| {
logging::warning!(self.logger, "Failed retrieving remaing-time.");
logging::warning!(self.logger, "This usually happens seeking into file end. Possibly mpv bug?");
logging::warning!(self.logger, "Defaulting to 0.");
0
});
Some(MpvEvent::Seek(self.get_remaining_time()))
}
Some(MpvEvent::Seek(remaining_time))
fn get_remaining_time(&self) -> i64 {
self.mpv.get_property("time-remaining").unwrap_or_default()
}
fn get_toggle_event(&self, message: ClientMessage) -> Option<MpvEvent> {
@ -115,10 +134,11 @@ impl MpvEventQueue {
logging::info!(self.logger, "Client message: {command}");
if command.starts_with("key-binding toggle-rpc d-") {
return Some(MpvEvent::Toggle)
Some(MpvEvent::Toggle)
}
else {
None
}
}
}

View file

@ -1,5 +1,3 @@
use std::fmt::Display;
pub struct FileInfo {
pub filename: String,
pub metadata: FileMetadata
@ -14,28 +12,22 @@ pub struct FileMetadata {
pub enum MpvEvent {
Toggle,
Play,
Pause,
Buffering,
Exit,
FileLoaded(FileInfo),
Play(i64),
Pause(i64),
Seek(i64)
}
impl Display for MpvEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let event_name = match self {
MpvEvent::Toggle => "Toggle",
MpvEvent::Play => "Play",
MpvEvent::Pause => "Pause",
MpvEvent::Exit => "Exit",
MpvEvent::FileLoaded(_) => "FileLoaded",
MpvEvent::Seek(_) => "Seek"
};
write!(f, "{}", event_name)
}
pub enum MpvRequest {
OSDMessage(&'static str)
}
pub trait MpvEventHandler {
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str>;
}
pub trait MpvRequester {
fn next_request<'a>(&mut self) -> Option<MpvRequest>;
}

View file

@ -1,9 +1,10 @@
use std::rc::Rc;
use mpv_client::mpv_handle;
use crate::logging::{self, Logger};
use crate::config::Config;
use crate::discord_client::DiscordClient;
use crate::mpv_event_queue::MpvEventQueue;
use crate::mpv_event_queue::events::{MpvEventHandler, MpvEvent};
use crate::mpv_event_queue::events::{MpvEventHandler, MpvRequester, MpvEvent, MpvRequest};
pub struct RPCPlugin {
logger: Rc<Logger>,
@ -14,8 +15,9 @@ pub struct RPCPlugin {
impl RPCPlugin {
pub fn new(handle: *mut mpv_handle, client_id: &str) -> Result<Self, &'static str> {
let logger = Rc::new(Logger::from_env());
let config = Config::from_config_file(&logger);
let mpv = MpvEventQueue::from_ptr(handle, Rc::clone(&logger))?;
let discord = DiscordClient::new(client_id, Rc::clone(&logger))?;
let discord = DiscordClient::new(client_id, config.active, config.cover_art, Rc::clone(&logger))?;
Ok(Self {
logger,
@ -28,7 +30,7 @@ impl RPCPlugin {
loop {
let event = self.mpv.next_event();
match event {
None => continue,
None => (),
Some(event) => {
if self.handle_event(event)
{
@ -36,6 +38,12 @@ impl RPCPlugin {
}
}
}
let request = self.discord.next_request();
match request {
None => (),
Some(request) => self.handle_request(request)
}
}
}
@ -51,4 +59,10 @@ impl RPCPlugin {
exit
}
fn handle_request(&self, request: MpvRequest) {
if let Err(e) = self.mpv.handle_request(request) {
logging::error!(self.logger, "Failed to handle mpv request: {e}");
}
}
}

15
src/utils.rs Normal file
View file

@ -0,0 +1,15 @@
pub fn truncate_string(current: &mut String, length: usize) {
match current.char_indices().nth(length) {
None => (),
Some((index, _)) => current.truncate(index)
}
}
pub fn truncate_string_fmt(current: &mut String, length: usize) {
if current.chars().count() <= length {
return;
}
truncate_string(current, length - 3);
current.push_str("...");
}