1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#[cfg(feature = "album-art")]
use crate::art::ArtFetcher;
use crate::mpris::MprisPropertiesChange;
use crate::mpris::PlayerMetadata;
use crate::mpris::PlayerStatus;
use crate::notifier::Notification;
use crate::DBusError;
use crate::{configuration::Configuration, dbus::DBusConnection, notifier::Notifier};
use rustbus::message_builder::MarshalledMessage;
use std::collections::HashMap;
use std::process::Command;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MessageHandlerError {
#[error("error handling D-Bus message")]
DBus(#[from] DBusError),
}
pub struct MessageHandler {
configuration: Configuration,
notifier: Notifier,
art_fetcher: ArtFetcher,
// Map from <D-Bus Sender> -> <Last Received Metadata>
metadata: HashMap<String, PlayerMetadata>,
// Map from <D-Bus Sender> -> <Last Received Status>
status: HashMap<String, PlayerStatus>,
// Notification that will be sent after [DEBOUNCE_PERIOD] passes.
pending_notification: Option<Notification>,
}
impl MessageHandler {
pub fn new(configuration: &Configuration) -> Self {
Self {
configuration: configuration.clone(),
notifier: Notifier::new(configuration),
art_fetcher: ArtFetcher::new(configuration),
metadata: HashMap::new(),
pending_notification: None,
status: HashMap::new(),
}
}
// Must be called regularly from the main loop. Used to fire notifications
// on a timer.
pub fn fire_pending(&mut self, dbus: &mut DBusConnection) -> Result<(), MessageHandlerError> {
if let Some(pending) = self.pending_notification.take() {
self.notifier.send_notification(pending, dbus)?;
self.fire_commands();
}
Ok(())
}
// Instantiates Command instances based on the configured commands.
fn generate_commands(&self) -> Vec<Command> {
let config_commands = self.configuration.commands.clone();
if config_commands.is_none() {
return Vec::new();
}
config_commands
.unwrap()
.iter()
.filter_map(|command_args| match command_args.len() {
0 => None,
1 => Some(Command::new(command_args[0].as_str())),
2.. => {
let mut cmd = Command::new(command_args[0].as_str());
cmd.args(&command_args[1..command_args.len()]);
Some(cmd)
}
})
.collect()
}
// Fires commands after a notification was sent.
fn fire_commands(&self) {
let mut commands = self.generate_commands();
for command in commands.iter_mut() {
match command.output() {
Ok(_) => (),
Err(err) => {
log::warn!("Command failed: {}", err);
}
}
}
}
// Called from the main loop for every received message. Sets the pending
// notification, but does not emit the notification; use [handle_pending]
// to send the notification.
pub fn process_message(
&mut self,
message: MarshalledMessage,
) -> Result<(), MessageHandlerError> {
let sender = message
.dynheader
.sender
.as_ref()
.ok_or_else(|| DBusError::Invalid("Missing sender header".to_string()))?
.clone();
let change = MprisPropertiesChange::try_from(message).ok();
// Signals we don't care about are ignored
if change.is_none() {
return Ok(());
}
let change = change.unwrap();
// Handle metadata property changes.
//
// Incoming metadata property changes are cached per each sender,
// where the most recently received metadata is cached in its
// entirety.
//
// A property change always queues up a notification to be sent.
let mut metadata: Option<&PlayerMetadata> = self.metadata.get(&sender);
if let Some(new_metadata) = change.metadata {
self.metadata
.insert(sender.to_string(), new_metadata.clone());
metadata = self.metadata.get(&sender);
// Wipe out player status whenever we have incoming track metadata.
// Player status is used to ensure that Playing -> Playing status
// changes don't generate spurious notifications.
self.status.remove(&sender);
// If our current notification is from the same sender, update it.
// Otherwise, wipe out whatever was being built and start
// hydrating a new Notification.
if let Some(pending) = self.pending_notification.as_mut() {
if pending.sender() == sender {
pending.update(&new_metadata, None);
}
} else {
self.pending_notification = Some(Notification::new(&sender, &new_metadata, None));
}
}
// If we haven't gotten metadata yet, we can't notify
if metadata.is_none() {
return Ok(());
}
let metadata = metadata.unwrap();
// Handle playback status.
//
// When the 'Playing' signal is sent, queue that sender's track
// for notification (either they're resuming play, or changing
// tracks).
if let Some(status) = change.status {
let last_status = self.status.insert(sender.clone(), status.clone());
if status == PlayerStatus::Playing {
// We only want to generate a notification for a "Playing" status
// change when we weren't already in "Playing".
if last_status.is_none() || last_status.is_some_and(|l| l != PlayerStatus::Playing)
{
self.pending_notification = Some(Notification::new(&sender, metadata, None));
}
} else {
self.pending_notification = None;
}
}
// We can't notify if the pending notification is still empty
if self.pending_notification.as_mut().is_none() {
return Ok(());
}
let pending = self.pending_notification.as_mut().unwrap();
// Fetch album art to a temporary buffer in the pending notification,
// if the feature is enabled.
#[cfg(feature = "album-art")]
if metadata.art_url.is_some() && self.configuration.enable_album_art {
let result = self
.art_fetcher
.get_album_art(metadata.art_url.as_ref().unwrap());
match result {
Ok(data) => {
pending.update(metadata, Some(data));
}
Err(err) => {
log::warn!("Error fetching album art for {:#?}: {}", &metadata, err);
}
}
}
Ok(())
}
}