-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathdbus.js
318 lines (297 loc) · 10.1 KB
/
dbus.js
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import GLib from "gi://GLib";
import Gio from "gi://Gio";
//dbus constants
const path = "/org/mpris/MediaPlayer2";
const interfaceName = "org.mpris.MediaPlayer2.Player";
const spotifyDbus = `<node>
<interface name="org.mpris.MediaPlayer2.Player">
<property name="PlaybackStatus" type="s" access="read"/>
<property name="Metadata" type="a{sv}" access="read"/>
<property name="Shuffle" type="b" access="read"/>
<property name="LoopStatus" type="s" access="read"/>
</interface>
</node>`;
/**
* This be the "list" of supported clients. At init, the extensions starts watching the session bus for each
* supported client.
*/
const supportedClients = [
{
name: "Spotify",
dest: "org.mpris.MediaPlayer2.spotify",
signal: null,
watchId: null,
isOnline: false, // to keep track of who appears and disappears in case multiple different clients are running
versions: [
{
name: "spotify version >1.84",
pattern: "/com/spotify",
idExtractor: (trackid) => trackid.split("/")[3],
},
{
name: "spotify version <1.84",
pattern: "spotify:",
idExtractor: (trackid) => trackid.split(":")[1],
},
],
},
{
name: "ncspot",
dest: "org.mpris.MediaPlayer2.ncspot",
signal: null,
watchId: null,
isOnline: false,
versions: [
{
name: "ncspot",
pattern: "/org/ncspot",
idExtractor: (trackid) => trackid.split("/")[4],
},
],
},
];
const SpTrayDbus = class SpTrayDbus {
constructor(panelButton) {
this.proxy = null;
this.panelButton = panelButton;
this.activeClient = null;
this.timeouts = [];
this.startWatching();
}
destroy() {
if (this.activeClient && this.proxy) {
this.proxy.disconnect(this.activeClient.signal);
}
for (const client in supportedClients) {
if (client.watchId) {
Gio.bus_unwatch_name(client.watchId);
}
}
for (const to of this.timeouts) {
GLib.Source.remove(to);
}
}
timeout() {
return new Promise((resolve) =>
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
resolve();
return GLib.SOURCE_REMOVE;
}),
);
}
startWatching() {
// Start the watch for the supported clients.
supportedClients.forEach((client) => {
client.watchId = Gio.bus_watch_name(
Gio.BusType.SESSION,
client.dest,
Gio.BusNameWatcherFlags.NONE,
this.onClientAppeared.bind(this, client),
this.onClientVanished.bind(this, client),
);
});
}
/**
* When a supported Spotify client's name appears on the session bus, create a proxy for it.
* This overrides the current proxy if there is one. Meaning the proxy is always for the most recently
* appeared client.
*/
async onClientAppeared(client) {
log(`${client.name} appeared on DBus.`);
this.makeProxyForClient(client);
// This is necessary because the proxy's property cache might be initialized with incomplete values,
// which needs to be updated after a short delay
try {
this.shouldRetry(this.proxy.Metadata);
} catch (error) {
logError(error);
}
if (!this.proxy.Metadata || this.shouldRetry(this.proxy.Metadata)) {
log(`Bad metadata, querying again.`);
try {
this.correctMetadata();
} catch (error) {
logError(error);
this.panelButton.updateLabel(true);
}
} else {
this.panelButton.updateLabel(true);
}
}
shouldRetry(metadata) {
// Don't check artist field, because it will be null/undefined for podcasts
return (
metadata["mpris:trackid"].unpack() == "" ||
metadata["xesam:album"].unpack() == "" ||
metadata["xesam:title"].unpack() == ""
);
}
/**
* Attempt to correct the proxy's incomplete Metadata cache
* Makes 5 attempts at 100ms intervals. Sets the panelButton text if succeeds.
*/
async correctMetadata() {
const maxAttempts = 5;
let attempt = 1;
do {
const resp = this.queryMetadata();
const unpacked = resp.deepUnpack();
if (!this.shouldRetry(unpacked)) {
log(`Got good metadata on attempt ${attempt}`);
try {
this.proxy.set_cached_property("Metadata", resp);
} catch (error) {
logError(error);
return;
}
this.panelButton.updateLabel(true);
return;
} else {
try {
this.timeouts.push(await this.timeout());
} catch (e) {
logError(e);
}
attempt++;
}
} while (attempt <= maxAttempts);
this.panelButton.showStopped();
}
/**
* Explicitly query the metadata property via DBus, instead of using the proxy cache.
*/
queryMetadata() {
// For some reason the "Get" DBus method returns weird stuff. Had to go with GetAll and
// pull Metadata out of it instead
const reply = Gio.DBus.session.call_sync(
this.activeClient.dest,
path,
"org.freedesktop.DBus.Properties",
"GetAll",
new GLib.Variant("(s)", [interfaceName]),
new GLib.VariantType("(a{sv})"),
Gio.DBusCallFlags.NONE,
-1,
null,
);
return reply.deepUnpack()[0]["Metadata"];
}
/**
* Create a proxy for a supported client, and connect the listen signal.
* Overrides the existing proxy, if there is one. Sets the currently active client to the most recently
* appeared.
*/
makeProxyForClient(client) {
if (this.activeClient && this.activeClient.name === client.name) {
return;
}
this.proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
Gio.DBusInterfaceInfo.new_for_xml(spotifyDbus),
client.dest,
path,
interfaceName,
null,
);
client.signal = this.proxy.connect(
"g-properties-changed",
(proxy, changed, invalidated) => {
const props = changed.deepUnpack();
// TODO simplify this mess
if (
!(
"PlaybackStatus" in props ||
"Metadata" in props ||
"LoopStatus" in props ||
"Shuffle" in props
)
) {
// None of the extension-relevant properties changed, nothing to do
return;
}
this.panelButton.updateLabel("Metadata" in props);
return;
},
);
client.isOnline = true;
if (this.activeClient) {
this.proxy.disconnect(this.activeClient.signal);
}
this.activeClient = client;
}
/**
* Runs when a client's name vanished from the session bus. Marks the vanished client as inactive.
* If the vanished client was the currently active one, looks for a replacement.
*/
onClientVanished(client) {
client.isOnline = false;
// Nothing to do if the client that vanished wasn't the one we were watching
if (this.proxy && client.dest !== this.proxy.get_name()) {
log(`${client.name} vanished from DBus.`);
return;
}
this.proxy.disconnect(client.signal);
this.activeClient = null;
log(`${client.name} vanished from DBus, looking for another client.`);
const otherClient = this.checkForOnlineClients();
if (!otherClient) {
log("No other Spotify clients online.");
this.proxy = null;
} else {
log(`${otherClient.name} is still online. Making it the primary.`);
this.makeProxyForClient(otherClient);
}
this.panelButton.updateLabel(true);
}
// Checks if any other supported client is online
checkForOnlineClients() {
for (const client of supportedClients) {
if (client.isOnline) {
return client;
}
}
return null;
}
/**
* Creates a metadata object that contains relevant information
* @returns title, artist, album and trackType. Artist is blank when it's a podcast.
*/
extractMetadataInformation() {
if (!this.proxy.Metadata || !this.proxy.Metadata["mpris:trackid"]) {
return null;
}
return {
trackType: this.getTrackType(this.proxy.Metadata["mpris:trackid"].get_string()[0]),
title: this.proxy.Metadata["xesam:title"].unpack(),
album: this.proxy.Metadata["xesam:album"].unpack(),
artist: this.proxy.Metadata["xesam:artist"].get_strv()[0],
url: this.proxy.Metadata["xesam:url"].unpack(),
};
}
getTrackType(trackId) {
for (const version of this.activeClient.versions) {
if (!trackId.startsWith(version.pattern)) {
continue;
}
return version.idExtractor(trackId);
}
return null;
}
spotifyIsActive() {
return this.proxy !== null;
}
getPlaybackStatus() {
return this.proxy.PlaybackStatus;
}
getPlaybackControl() {
if (!this.proxy || !this.proxy.Shuffle || !this.proxy.LoopStatus) {
return null;
}
return {
shuffle: this.proxy.Shuffle,
loop: this.proxy.LoopStatus,
};
}
};
export default SpTrayDbus;