Added cover art, buffering, config, string truncating
This commit is contained in:
parent
0cce7cbcd9
commit
b87a0cf5d9
10 changed files with 320 additions and 64 deletions
|
@ -10,6 +10,9 @@ crate-type = ["cdylib"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mpv-client = "0.4.1"
|
mpv-client = "0.4.1"
|
||||||
discord-rich-presence="0.2.3"
|
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]
|
[profile.release-full]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|
76
src/config.rs
Normal file
76
src/config.rs
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,56 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use crate::logging::{self, Logger};
|
use std::collections::VecDeque;
|
||||||
use crate::mpv_event_queue::events::{MpvEventHandler, MpvEvent, FileInfo};
|
|
||||||
use discord_rich_presence::{DiscordIpcClient, DiscordIpc};
|
use discord_rich_presence::{DiscordIpcClient, DiscordIpc};
|
||||||
use discord_rich_presence::activity::{Activity, Assets, Timestamps};
|
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 {
|
struct ActivityInfo {
|
||||||
details: String,
|
details: String,
|
||||||
state: String,
|
state: String,
|
||||||
|
assets: AssetsInfo,
|
||||||
timestamps: Timestamps
|
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 {
|
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 {
|
Self {
|
||||||
details,
|
details,
|
||||||
state,
|
state,
|
||||||
|
assets,
|
||||||
timestamps
|
timestamps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,15 +59,14 @@ impl ActivityInfo {
|
||||||
Self {
|
Self {
|
||||||
details: String::new(),
|
details: String::new(),
|
||||||
state: String::new(),
|
state: String::new(),
|
||||||
|
assets: AssetsInfo::empty(),
|
||||||
timestamps: Timestamps::new()
|
timestamps: Timestamps::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get_activity(&self) -> Activity {
|
pub fn get_activity(&self) -> Activity {
|
||||||
let assets = Assets::new()
|
let assets = self.assets.get_assets();
|
||||||
.large_image("logo")
|
|
||||||
.large_text("mpv");
|
|
||||||
|
|
||||||
Activity::new()
|
Activity::new()
|
||||||
.assets(assets)
|
.assets(assets)
|
||||||
|
@ -43,26 +77,36 @@ impl ActivityInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct DiscordClient {
|
pub struct DiscordClient {
|
||||||
logger: Rc<Logger>,
|
|
||||||
discord: DiscordIpcClient,
|
discord: DiscordIpcClient,
|
||||||
activity_info: ActivityInfo,
|
activity_info: ActivityInfo,
|
||||||
active: bool
|
active: bool,
|
||||||
|
cover_art: bool,
|
||||||
|
mpv_requests: VecDeque<MpvRequest>,
|
||||||
|
logger: Rc<Logger>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiscordClient {
|
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) {
|
let discord = match DiscordIpcClient::new(client_id) {
|
||||||
Ok(discord) => discord,
|
Ok(discord) => discord,
|
||||||
Err(_) => return Err("cannot init discord client")
|
Err(_) => return Err("cannot init discord client")
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
let mut new_self = Self {
|
||||||
logger,
|
|
||||||
discord,
|
discord,
|
||||||
activity_info: ActivityInfo::empty(),
|
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 {
|
fn get_state(file_info: &FileInfo) -> String {
|
||||||
|
@ -76,6 +120,7 @@ impl DiscordClient {
|
||||||
if let Some(album) = &metadata.album {
|
if let Some(album) = &metadata.album {
|
||||||
state += &format!(" on {album}");
|
state += &format!(" on {album}");
|
||||||
}
|
}
|
||||||
|
utils::truncate_string_fmt(&mut state, MAX_STR_LEN);
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +131,38 @@ impl DiscordClient {
|
||||||
None => return file_info.filename.clone()
|
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}] ")
|
format!("{title} [T{track}] ")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
title.clone()
|
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> {
|
fn update_presence(&mut self) -> Result<(), &'static str> {
|
||||||
|
@ -105,19 +176,23 @@ impl DiscordClient {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
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> {
|
fn set_presence(&mut self, file_info: FileInfo) -> Result<(), &'static str> {
|
||||||
let details = DiscordClient::get_details(&file_info);
|
let details = DiscordClient::get_details(&file_info);
|
||||||
let state = DiscordClient::get_state(&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()
|
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 = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH);
|
||||||
let current_time = match current_time {
|
let current_time = match current_time {
|
||||||
Ok(time) => time.as_secs() as i64,
|
Ok(time) => time.as_secs() as i64,
|
||||||
|
@ -130,6 +205,11 @@ impl DiscordClient {
|
||||||
self.update_presence()
|
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> {
|
fn open(&mut self) -> Result<(), &'static str> {
|
||||||
if self.active {
|
if self.active {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -139,6 +219,7 @@ impl DiscordClient {
|
||||||
match self.discord.connect() {
|
match self.discord.connect() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.active = true;
|
self.active = true;
|
||||||
|
self.request_osd_message("Discord RPC started");
|
||||||
self.update_presence()
|
self.update_presence()
|
||||||
}
|
}
|
||||||
Err(_) => Err("cannot connect to Discord")
|
Err(_) => Err("cannot connect to Discord")
|
||||||
|
@ -154,6 +235,7 @@ impl DiscordClient {
|
||||||
match self.discord.close() {
|
match self.discord.close() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
|
self.request_osd_message("Discord RPC stopped");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(_) => Err("cannot disconnect from Discord")
|
Err(_) => Err("cannot disconnect from Discord")
|
||||||
|
@ -166,16 +248,28 @@ impl DiscordClient {
|
||||||
true => self.close()
|
true => self.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn request_osd_message(&mut self, message: &'static str) {
|
||||||
|
self.mpv_requests.push_front(MpvRequest::OSDMessage(message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MpvEventHandler for DiscordClient {
|
impl MpvEventHandler for DiscordClient {
|
||||||
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str> {
|
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str> {
|
||||||
match event {
|
match event {
|
||||||
MpvEvent::FileLoaded(file_info) => self.set_presence(file_info),
|
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::Toggle => self.toggle_activity(),
|
||||||
MpvEvent::Exit => self.close(),
|
MpvEvent::Exit => self.close(),
|
||||||
_ => Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MpvRequester for DiscordClient {
|
||||||
|
fn next_request<'a>(&mut self) -> Option<MpvRequest> {
|
||||||
|
self.mpv_requests.pop_back()
|
||||||
|
}
|
||||||
|
}
|
39
src/discord_client/music_brainz.rs
Normal file
39
src/discord_client/music_brainz.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
use mpv_client::mpv_handle;
|
use mpv_client::mpv_handle;
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod mpv_event_queue;
|
mod mpv_event_queue;
|
||||||
mod discord_client;
|
mod discord_client;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use plugin::RPCPlugin;
|
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 {
|
fn mpv_open_cplugin(handle: *mut mpv_handle) -> std::os::raw::c_int {
|
||||||
let plugin = match RPCPlugin::new(handle, DISCORD_APPID) {
|
let plugin = match RPCPlugin::new(handle, DISCORD_APPID) {
|
||||||
Ok(plugin) => plugin,
|
Ok(plugin) => plugin,
|
||||||
Err(_) => return -1
|
Err(e) => {
|
||||||
|
println!("Error creating RPC plugin: {e}");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
plugin.run();
|
plugin.run();
|
||||||
|
|
|
@ -19,12 +19,11 @@ impl From<u32> for LogLevel {
|
||||||
1 => LogLevel::Error,
|
1 => LogLevel::Error,
|
||||||
2 => LogLevel::Warn,
|
2 => LogLevel::Warn,
|
||||||
3 => LogLevel::Info,
|
3 => LogLevel::Info,
|
||||||
_ => LogLevel::Error
|
_ => LogLevel::Info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct Logger {
|
pub struct Logger {
|
||||||
log_level: LogLevel
|
log_level: LogLevel
|
||||||
|
@ -65,5 +64,4 @@ impl Logger {
|
||||||
println!("[mpv-rpc (ERROR)] {}", message);
|
println!("[mpv-rpc (ERROR)] {}", message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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 mpv_client::{Handle, Event, Property, Format, mpv_handle, ClientMessage};
|
||||||
use crate::logging::{self, Logger};
|
use crate::logging::{self, Logger};
|
||||||
|
|
||||||
pub mod events;
|
pub mod events;
|
||||||
use events::{MpvEvent, FileInfo, FileMetadata};
|
use events::{MpvEvent, MpvRequest, FileInfo, FileMetadata};
|
||||||
|
|
||||||
|
|
||||||
const NAME_PAUSE_PROP: &str = "pause";
|
const NAME_PAUSE_PROP: &str = "pause";
|
||||||
|
@ -11,7 +11,7 @@ const REPL_PAUSE_PROP: u64 = 1;
|
||||||
|
|
||||||
pub struct MpvEventQueue {
|
pub struct MpvEventQueue {
|
||||||
mpv: Handle,
|
mpv: Handle,
|
||||||
logger: Rc<Logger>,
|
logger: Rc<Logger>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MpvEventQueue {
|
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 event = self.mpv.wait_event(0.0);
|
||||||
let mpv_event = self.convert_event(event);
|
let mpv_event = self.convert_event(event);
|
||||||
|
|
||||||
if let Some(ref event) = mpv_event {
|
|
||||||
logging::info!(self.logger, "Event: {event}");
|
|
||||||
}
|
|
||||||
|
|
||||||
mpv_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> {
|
fn convert_event(&self, event: Event) -> Option<MpvEvent> {
|
||||||
|
match event {
|
||||||
|
Event::None => (),
|
||||||
|
ref event => logging::info!(self.logger, "Event: {event}")
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::FileLoaded => self.get_file_info_event(),
|
Event::FileLoaded => self.get_file_info_event(),
|
||||||
Event::PlaybackRestart => self.get_seek_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> {
|
fn get_property_event(&self, prop_id: u64, prop: Property) -> Option<MpvEvent> {
|
||||||
logging::info!(self.logger, "Property changed: {prop_id}");
|
logging::info!(self.logger, "Property changed: {prop_id}");
|
||||||
match 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
|
_ => 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 {
|
match pause {
|
||||||
false => MpvEvent::Play,
|
false => Some(MpvEvent::Play(time)),
|
||||||
true => MpvEvent::Pause
|
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> {
|
fn get_seek_event(&self) -> Option<MpvEvent> {
|
||||||
let remaining_time = self.mpv.get_property("time-remaining").unwrap_or_else(|_| {
|
Some(MpvEvent::Seek(self.get_remaining_time()))
|
||||||
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(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> {
|
fn get_toggle_event(&self, message: ClientMessage) -> Option<MpvEvent> {
|
||||||
|
@ -115,10 +134,11 @@ impl MpvEventQueue {
|
||||||
logging::info!(self.logger, "Client message: {command}");
|
logging::info!(self.logger, "Client message: {command}");
|
||||||
|
|
||||||
if command.starts_with("key-binding toggle-rpc d-") {
|
if command.starts_with("key-binding toggle-rpc d-") {
|
||||||
return Some(MpvEvent::Toggle)
|
Some(MpvEvent::Toggle)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
pub struct FileInfo {
|
pub struct FileInfo {
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub metadata: FileMetadata
|
pub metadata: FileMetadata
|
||||||
|
@ -14,28 +12,22 @@ pub struct FileMetadata {
|
||||||
|
|
||||||
pub enum MpvEvent {
|
pub enum MpvEvent {
|
||||||
Toggle,
|
Toggle,
|
||||||
Play,
|
Buffering,
|
||||||
Pause,
|
|
||||||
Exit,
|
Exit,
|
||||||
FileLoaded(FileInfo),
|
FileLoaded(FileInfo),
|
||||||
|
Play(i64),
|
||||||
|
Pause(i64),
|
||||||
Seek(i64)
|
Seek(i64)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for MpvEvent {
|
pub enum MpvRequest {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
OSDMessage(&'static str)
|
||||||
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 trait MpvEventHandler {
|
pub trait MpvEventHandler {
|
||||||
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str>;
|
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait MpvRequester {
|
||||||
|
fn next_request<'a>(&mut self) -> Option<MpvRequest>;
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use mpv_client::mpv_handle;
|
use mpv_client::mpv_handle;
|
||||||
use crate::logging::{self, Logger};
|
use crate::logging::{self, Logger};
|
||||||
|
use crate::config::Config;
|
||||||
use crate::discord_client::DiscordClient;
|
use crate::discord_client::DiscordClient;
|
||||||
use crate::mpv_event_queue::MpvEventQueue;
|
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 {
|
pub struct RPCPlugin {
|
||||||
logger: Rc<Logger>,
|
logger: Rc<Logger>,
|
||||||
|
@ -14,8 +15,9 @@ pub struct RPCPlugin {
|
||||||
impl RPCPlugin {
|
impl RPCPlugin {
|
||||||
pub fn new(handle: *mut mpv_handle, client_id: &str) -> Result<Self, &'static str> {
|
pub fn new(handle: *mut mpv_handle, client_id: &str) -> Result<Self, &'static str> {
|
||||||
let logger = Rc::new(Logger::from_env());
|
let logger = Rc::new(Logger::from_env());
|
||||||
|
let config = Config::from_config_file(&logger);
|
||||||
let mpv = MpvEventQueue::from_ptr(handle, Rc::clone(&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 {
|
Ok(Self {
|
||||||
logger,
|
logger,
|
||||||
|
@ -28,7 +30,7 @@ impl RPCPlugin {
|
||||||
loop {
|
loop {
|
||||||
let event = self.mpv.next_event();
|
let event = self.mpv.next_event();
|
||||||
match event {
|
match event {
|
||||||
None => continue,
|
None => (),
|
||||||
Some(event) => {
|
Some(event) => {
|
||||||
if self.handle_event(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
|
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
15
src/utils.rs
Normal 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("...");
|
||||||
|
}
|
Loading…
Reference in a new issue