Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-mpris-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <math.h>
9 : :
10 : : #include <glib/gi18n.h>
11 : : #include <gio/gio.h>
12 : : #include <json-glib/json-glib.h>
13 : : #include <valent.h>
14 : :
15 : : #include "valent-mpris-device.h"
16 : : #include "valent-mpris-plugin.h"
17 : : #include "valent-mpris-utils.h"
18 : :
19 : :
20 : : struct _ValentMprisPlugin
21 : : {
22 : : ValentDevicePlugin parent_instance;
23 : :
24 : : ValentMedia *media;
25 : : unsigned int media_watch : 1;
26 : :
27 : : GPtrArray *players;
28 : : GHashTable *transfers;
29 : :
30 : : GHashTable *pending;
31 : : unsigned int flush_id;
32 : : };
33 : :
34 [ + + + - ]: 98 : G_DEFINE_FINAL_TYPE (ValentMprisPlugin, valent_mpris_plugin, VALENT_TYPE_DEVICE_PLUGIN)
35 : :
36 : : static void valent_mpris_plugin_send_player_info (ValentMprisPlugin *self,
37 : : ValentMediaPlayer *player,
38 : : gboolean now_playing,
39 : : gboolean volume);
40 : : static void valent_mpris_plugin_send_player_list (ValentMprisPlugin *self);
41 : :
42 : :
43 : : static gpointer
44 : 11 : _valent_media_lookup_player (ValentMedia *media,
45 : : const char *name)
46 : : {
47 : 11 : unsigned int n_players = 0;
48 : :
49 [ + - ]: 11 : g_assert (VALENT_IS_MEDIA (media));
50 [ + - - + ]: 11 : g_assert (name != NULL && *name != '\0');
51 : :
52 : 11 : n_players = g_list_model_get_n_items (G_LIST_MODEL (media));
53 : :
54 [ + - ]: 11 : for (unsigned int i = 0; i < n_players; i++)
55 : : {
56 : 11 : g_autoptr (ValentMediaPlayer) player = g_list_model_get_item (G_LIST_MODEL (media), i);
57 : :
58 [ + - ]: 11 : if (g_strcmp0 (valent_media_player_get_name (player), name) == 0)
59 [ + - ]: 11 : return player;
60 : : }
61 : :
62 : : return NULL;
63 : : }
64 : :
65 : :
66 : : /*
67 : : * Local Players
68 : : */
69 : : static void
70 : 1 : send_album_art_cb (ValentTransfer *transfer,
71 : : GAsyncResult *result,
72 : : ValentMprisPlugin *self)
73 : : {
74 : 2 : g_autoptr (GError) error = NULL;
75 [ - + ]: 1 : g_autofree char *id = NULL;
76 : :
77 [ + - ]: 1 : g_assert (VALENT_IS_TRANSFER (transfer));
78 : :
79 [ - + ]: 1 : if (!valent_transfer_execute_finish (transfer, result, &error))
80 : 0 : g_debug ("Failed to upload album art: %s", error->message);
81 : :
82 : 1 : id = valent_transfer_dup_id (transfer);
83 : 1 : g_hash_table_remove (self->transfers, id);
84 : 1 : }
85 : :
86 : : static void
87 : 1 : valent_mpris_plugin_send_album_art (ValentMprisPlugin *self,
88 : : ValentMediaPlayer *player,
89 : : const char *requested_uri)
90 : : {
91 : 1 : g_autoptr (GVariant) metadata = NULL;
92 : 1 : const char *real_uri;
93 [ - - ]: 1 : g_autoptr (GFile) real_file = NULL;
94 [ + - - - ]: 1 : g_autoptr (GFile) requested_file = NULL;
95 [ + - - - ]: 1 : g_autoptr (JsonBuilder) builder = NULL;
96 [ - + - - ]: 1 : g_autoptr (JsonNode) packet = NULL;
97 [ + - - - ]: 1 : g_autoptr (ValentTransfer) transfer = NULL;
98 : 1 : ValentDevice *device;
99 : :
100 [ + - ]: 1 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
101 : :
102 : : /* Ignore concurrent requests */
103 [ + - ]: 1 : if (g_hash_table_contains (self->transfers, requested_uri))
104 : : return;
105 : :
106 : : /* Check player and URL are safe */
107 [ + - - + ]: 2 : if ((metadata = valent_media_player_get_metadata (player)) == NULL ||
108 : 1 : !g_variant_lookup (metadata, "mpris:artUrl", "&s", &real_uri))
109 : : {
110 : 0 : g_warning ("Album art request \"%s\" for track without album art",
111 : : requested_uri);
112 : 0 : return;
113 : : }
114 : :
115 : : /* Compare normalized URLs */
116 : 1 : requested_file = g_file_new_for_uri (requested_uri);
117 : 1 : real_file = g_file_new_for_uri (real_uri);
118 : :
119 [ - + ]: 1 : if (!g_file_equal (requested_file, real_file))
120 : : {
121 : 0 : g_warning ("Album art request \"%s\" doesn't match current track \"%s\"",
122 : : requested_uri, real_uri);
123 : 0 : return;
124 : : }
125 : :
126 : : /* Build the payload packet */
127 : 1 : valent_packet_init (&builder, "kdeconnect.mpris");
128 : 1 : json_builder_set_member_name (builder, "player");
129 : 1 : json_builder_add_string_value (builder, valent_media_player_get_name (player));
130 : 1 : json_builder_set_member_name (builder, "albumArtUrl");
131 : 1 : json_builder_add_string_value (builder, requested_uri);
132 : 1 : json_builder_set_member_name (builder, "transferringAlbumArt");
133 : 1 : json_builder_add_boolean_value (builder, TRUE);
134 : 1 : packet = valent_packet_end (&builder);
135 : :
136 : : /* Start the transfer */
137 : 1 : device = valent_extension_get_object (VALENT_EXTENSION (self));
138 : 1 : transfer = valent_device_transfer_new (device, packet, real_file);
139 : :
140 [ - + ]: 1 : g_hash_table_insert (self->transfers,
141 : 1 : g_strdup (requested_uri),
142 : : g_object_ref (transfer));
143 : :
144 [ + - ]: 1 : valent_transfer_execute (transfer,
145 : : NULL,
146 : : (GAsyncReadyCallback)send_album_art_cb,
147 : : self);
148 : : }
149 : :
150 : : static gboolean
151 : 9 : valent_mpris_plugin_flush (gpointer data)
152 : : {
153 : 9 : ValentMprisPlugin *self = VALENT_MPRIS_PLUGIN (data);
154 : 9 : GHashTableIter iter;
155 : 9 : ValentMediaPlayer *player;
156 : :
157 : 9 : g_hash_table_iter_init (&iter, self->pending);
158 [ + + ]: 18 : while (g_hash_table_iter_next (&iter, (void **)&player, NULL))
159 : : {
160 : 9 : valent_mpris_plugin_send_player_info (self, player, TRUE, TRUE);
161 : 9 : g_hash_table_iter_remove (&iter);
162 : : }
163 : :
164 : 9 : self->flush_id = 0;
165 : :
166 : 9 : return G_SOURCE_REMOVE;
167 : : }
168 : :
169 : : static void
170 : 2 : on_player_seeked (ValentMediaPlayer *player,
171 : : double position,
172 : : ValentMprisPlugin *self)
173 : : {
174 : 2 : const char *name;
175 : 4 : g_autoptr (JsonBuilder) builder = NULL;
176 [ - + ]: 2 : g_autoptr (JsonNode) packet = NULL;
177 : :
178 [ + - ]: 2 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
179 : :
180 : 2 : name = valent_media_player_get_name (player);
181 : :
182 : : /* Convert seconds to milliseconds */
183 : 2 : valent_packet_init (&builder, "kdeconnect.mpris");
184 : 2 : json_builder_set_member_name (builder, "player");
185 : 2 : json_builder_add_string_value (builder, name);
186 : 2 : json_builder_set_member_name (builder, "pos");
187 : 2 : json_builder_add_int_value (builder, (int64_t)(position * 1000L));
188 : 2 : packet = valent_packet_end (&builder);
189 : :
190 [ + - ]: 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
191 : 2 : }
192 : :
193 : : static void
194 : 21 : on_player_changed (ValentMediaPlayer *player,
195 : : GParamSpec *pspec,
196 : : ValentMprisPlugin *self)
197 : : {
198 [ + - ]: 21 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
199 : :
200 [ + + ]: 21 : if (g_str_equal (pspec->name, "position"))
201 : : {
202 : 2 : double position = 0.0;
203 : :
204 : 2 : position = valent_media_player_get_position (player);
205 : 2 : on_player_seeked (player, position, self);
206 : : }
207 : : else
208 : : {
209 : 19 : g_hash_table_add (self->pending, player);
210 : :
211 [ + + ]: 19 : if (self->flush_id == 0)
212 : 9 : self->flush_id = g_idle_add (valent_mpris_plugin_flush, self);
213 : : }
214 : 21 : }
215 : :
216 : : static void
217 : 3 : on_players_changed (ValentMedia *media,
218 : : unsigned int position,
219 : : unsigned int removed,
220 : : unsigned int added,
221 : : ValentMprisPlugin *self)
222 : : {
223 : 3 : gboolean changed = FALSE;
224 : :
225 [ + + ]: 3 : if (removed > 0)
226 : : {
227 : 2 : changed = TRUE;
228 : 2 : g_hash_table_remove_all (self->pending);
229 : : }
230 : :
231 [ + + ]: 4 : for (unsigned int i = 0; i < added; i++)
232 : : {
233 : 1 : g_autoptr (ValentMediaPlayer) player = NULL;
234 : :
235 : 1 : player = g_list_model_get_item (G_LIST_MODEL (media), position + i);
236 : :
237 : : /* Here, and below when building the player list, all `ValentMprisDevice`
238 : : * players are being skipped. An advanced option could control whether
239 : : * `!g_ptr_array_find (self->players, player, NULL)` passes, enabling a
240 : : * device to act as a hub for other devices. */
241 [ + - ]: 1 : if (VALENT_IS_MPRIS_DEVICE (player))
242 [ + - ]: 1 : continue;
243 : :
244 : 0 : changed = TRUE;
245 : 0 : g_signal_connect_object (player,
246 : : "notify",
247 : : G_CALLBACK (on_player_changed),
248 : : self, 0);
249 : :
250 [ # # ]: 0 : VALENT_NOTE ("tracking %s (%s)",
251 : : G_OBJECT_TYPE_NAME (player),
252 : : valent_media_player_get_name (player));
253 : : }
254 : :
255 [ + + ]: 3 : if (changed)
256 : 2 : valent_mpris_plugin_send_player_list (self);
257 : 3 : }
258 : :
259 : : static void
260 : 5 : valent_mpris_plugin_handle_action (ValentMprisPlugin *self,
261 : : ValentMediaPlayer *player,
262 : : const char *action)
263 : : {
264 [ + - ]: 5 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
265 [ - + ]: 5 : g_assert (VALENT_IS_MEDIA_PLAYER (player));
266 [ + - - + ]: 5 : g_assert (action && *action);
267 : :
268 [ + + ]: 5 : if (g_str_equal (action, "Next"))
269 : 1 : valent_media_player_next (player);
270 : :
271 [ + + ]: 4 : else if (g_str_equal (action, "Pause"))
272 : 1 : valent_media_player_pause (player);
273 : :
274 [ + + ]: 3 : else if (g_str_equal (action, "Play"))
275 : 1 : valent_media_player_play (player);
276 : :
277 [ - + ]: 2 : else if (g_str_equal (action, "PlayPause"))
278 [ # # # # : 0 : valent_mpris_play_pause (player);
# # ]
279 : :
280 [ + + ]: 2 : else if (g_str_equal (action, "Previous"))
281 : 1 : valent_media_player_previous (player);
282 : :
283 [ + - ]: 1 : else if (g_str_equal (action, "Stop"))
284 : 1 : valent_media_player_stop (player);
285 : :
286 : : else
287 : 0 : g_warning ("%s(): Unknown action: %s", G_STRFUNC, action);
288 : 5 : }
289 : :
290 : : static void
291 : 11 : valent_mpris_plugin_handle_mpris_request (ValentMprisPlugin *self,
292 : : JsonNode *packet)
293 : : {
294 : 11 : ValentMediaPlayer *player = NULL;
295 : 11 : const char *name;
296 : 11 : const char *action;
297 : 11 : const char *url;
298 : 11 : int64_t offset_us;
299 : 11 : int64_t position;
300 : 11 : gboolean request_now_playing;
301 : 11 : gboolean request_volume;
302 : 11 : const char *loop_status;
303 : 11 : ValentMediaRepeat repeat;
304 : 11 : gboolean shuffle;
305 : 11 : int64_t volume;
306 : :
307 [ + - ]: 11 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
308 : :
309 : : /* Start by checking for a player */
310 [ + - ]: 11 : if (valent_packet_get_string (packet, "player", &name))
311 : 11 : player = _valent_media_lookup_player (self->media, name);
312 : :
313 [ + - - + ]: 11 : if (player == NULL || valent_packet_check_field (packet, "requestPlayerList"))
314 : : {
315 : 0 : valent_mpris_plugin_send_player_list (self);
316 : 0 : return;
317 : : }
318 : :
319 : : /* A request for a player's status */
320 : 11 : request_now_playing = valent_packet_check_field (packet, "requestNowPlaying");
321 : 11 : request_volume = valent_packet_check_field (packet, "requestVolume");
322 : :
323 [ + + ]: 11 : if (request_now_playing || request_volume)
324 : 1 : valent_mpris_plugin_send_player_info (self,
325 : : player,
326 : : request_now_playing,
327 : : request_volume);
328 : :
329 : : /* A player command */
330 [ + + ]: 11 : if (valent_packet_get_string (packet, "action", &action))
331 : 5 : valent_mpris_plugin_handle_action (self, player, action);
332 : :
333 : : /* A request to change the relative position (microseconds to seconds) */
334 [ + + ]: 11 : if (valent_packet_get_int (packet, "Seek", &offset_us))
335 : 1 : valent_media_player_seek (player, offset_us / G_TIME_SPAN_SECOND);
336 : :
337 : : /* A request to change the absolute position (milliseconds to seconds) */
338 [ - + ]: 11 : if (valent_packet_get_int (packet, "SetPosition", &position))
339 : 0 : valent_media_player_set_position (player, position / 1000L);
340 : :
341 : : /* A request to change the loop status */
342 [ + + ]: 11 : if (valent_packet_get_string (packet, "setLoopStatus", &loop_status))
343 : : {
344 : 1 : repeat = valent_mpris_repeat_from_string (loop_status);
345 : 1 : valent_media_player_set_repeat (player, repeat);
346 : : }
347 : :
348 : : /* A request to change the shuffle mode */
349 [ + + ]: 11 : if (valent_packet_get_boolean (packet, "setShuffle", &shuffle))
350 : 1 : valent_media_player_set_shuffle (player, shuffle);
351 : :
352 : : /* A request to change the player volume */
353 [ + + ]: 11 : if (valent_packet_get_int (packet, "setVolume", &volume))
354 : 1 : valent_media_player_set_volume (player, volume / 100.0);
355 : :
356 : : /* An album art request */
357 [ + + ]: 11 : if (valent_packet_get_string (packet, "albumArtUrl", &url))
358 : 1 : valent_mpris_plugin_send_album_art (self, player, url);
359 : : }
360 : :
361 : : static void
362 : 10 : valent_mpris_plugin_send_player_info (ValentMprisPlugin *self,
363 : : ValentMediaPlayer *player,
364 : : gboolean request_now_playing,
365 : : gboolean request_volume)
366 : : {
367 : 10 : const char *name;
368 : 20 : g_autoptr (JsonBuilder) builder = NULL;
369 [ - + ]: 10 : g_autoptr (JsonNode) response = NULL;
370 : :
371 [ + - ]: 10 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
372 [ - + ]: 10 : g_assert (VALENT_IS_MEDIA_PLAYER (player));
373 : :
374 : : /* Start the packet */
375 : 10 : valent_packet_init (&builder, "kdeconnect.mpris");
376 : :
377 : 10 : name = valent_media_player_get_name (player);
378 : 10 : json_builder_set_member_name (builder, "player");
379 : 10 : json_builder_add_string_value (builder, name);
380 : :
381 : : /* Player State & Metadata */
382 [ + - ]: 10 : if (request_now_playing)
383 : : {
384 : 10 : ValentMediaActions flags;
385 : 10 : ValentMediaRepeat repeat;
386 : 10 : gboolean is_playing;
387 : 10 : double position;
388 : 10 : gboolean shuffle;
389 : 10 : const char *loop_status = "None";
390 : 10 : g_autoptr (GVariant) metadata = NULL;
391 [ + - ]: 10 : g_autofree char *artist = NULL;
392 : 10 : const char *title = NULL;
393 : :
394 : : /* Player State */
395 : 10 : flags = valent_media_player_get_flags (player);
396 : 10 : json_builder_set_member_name (builder, "canPause");
397 : 10 : json_builder_add_boolean_value (builder, (flags & VALENT_MEDIA_ACTION_PAUSE) != 0);
398 : 10 : json_builder_set_member_name (builder, "canPlay");
399 : 10 : json_builder_add_boolean_value (builder, (flags & VALENT_MEDIA_ACTION_PLAY) != 0);
400 : 10 : json_builder_set_member_name (builder, "canGoNext");
401 : 10 : json_builder_add_boolean_value (builder, (flags & VALENT_MEDIA_ACTION_NEXT) != 0);
402 : 10 : json_builder_set_member_name (builder, "canGoPrevious");
403 : 10 : json_builder_add_boolean_value (builder,(flags & VALENT_MEDIA_ACTION_PREVIOUS) != 0);
404 : 10 : json_builder_set_member_name (builder, "canSeek");
405 : 10 : json_builder_add_boolean_value (builder, (flags & VALENT_MEDIA_ACTION_SEEK) != 0);
406 : :
407 : 10 : repeat = valent_media_player_get_repeat (player);
408 : 10 : loop_status = valent_mpris_repeat_to_string (repeat);
409 : 10 : json_builder_set_member_name (builder, "loopStatus");
410 : 10 : json_builder_add_string_value (builder, loop_status);
411 : :
412 : 10 : shuffle = valent_media_player_get_shuffle (player);
413 : 10 : json_builder_set_member_name (builder, "shuffle");
414 : 10 : json_builder_add_boolean_value (builder, shuffle);
415 : :
416 : 10 : is_playing = valent_media_player_get_state (player) == VALENT_MEDIA_STATE_PLAYING;
417 : 10 : json_builder_set_member_name (builder, "isPlaying");
418 : 10 : json_builder_add_boolean_value (builder, is_playing);
419 : :
420 : : /* Convert seconds to milliseconds */
421 : 10 : position = valent_media_player_get_position (player);
422 : 10 : json_builder_set_member_name (builder, "pos");
423 : 10 : json_builder_add_int_value (builder, (int64_t)(position * 1000L));
424 : :
425 : : /* Track Metadata
426 : : *
427 : : * See: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/
428 : : */
429 [ + - ]: 10 : if ((metadata = valent_media_player_get_metadata (player)) != NULL)
430 : : {
431 : 10 : g_autofree const char **artists = NULL;
432 : 10 : int64_t length_us;
433 : 10 : const char *art_url;
434 : 10 : const char *album;
435 : :
436 [ + + ]: 10 : if (g_variant_lookup (metadata, "xesam:artist", "^a&s", &artists) &&
437 [ + - + - ]: 4 : artists[0] != NULL && *artists[0] != '\0')
438 : : {
439 : 4 : artist = g_strjoinv (", ", (char **)artists);
440 : 4 : json_builder_set_member_name (builder, "artist");
441 : 4 : json_builder_add_string_value (builder, artist);
442 : : }
443 : :
444 [ + + ]: 10 : if (g_variant_lookup (metadata, "xesam:title", "&s", &title) &&
445 [ + - ]: 4 : *title != '\0')
446 : : {
447 : 4 : json_builder_set_member_name (builder, "title");
448 : 4 : json_builder_add_string_value (builder, title);
449 : : }
450 : :
451 [ + + ]: 10 : if (g_variant_lookup (metadata, "xesam:album", "&s", &album) &&
452 [ + - ]: 4 : *album != '\0')
453 : : {
454 : 4 : json_builder_set_member_name (builder, "album");
455 : 4 : json_builder_add_string_value (builder, album);
456 : : }
457 : :
458 : : /* Convert microseconds to milliseconds */
459 [ + + ]: 10 : if (g_variant_lookup (metadata, "mpris:length", "x", &length_us))
460 : : {
461 : 4 : json_builder_set_member_name (builder, "length");
462 : 4 : json_builder_add_int_value (builder, length_us / 1000L);
463 : : }
464 : :
465 [ + + ]: 10 : if (g_variant_lookup (metadata, "mpris:artUrl", "&s", &art_url))
466 : : {
467 : 1 : json_builder_set_member_name (builder, "albumArtUrl");
468 : 1 : json_builder_add_string_value (builder, art_url);
469 : : }
470 : : }
471 : : }
472 : :
473 : : /* Volume Level */
474 [ + - ]: 10 : if (request_volume)
475 : : {
476 : 10 : int64_t level;
477 : :
478 : 10 : level = (int64_t)floor (valent_media_player_get_volume (player) * 100);
479 : 10 : json_builder_set_member_name (builder, "volume");
480 : 10 : json_builder_add_int_value (builder, level);
481 : : }
482 : :
483 : : /* Send Response */
484 : 10 : response = valent_packet_end (&builder);
485 [ + - ]: 10 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), response);
486 : 10 : }
487 : :
488 : : static void
489 : 5 : valent_mpris_plugin_send_player_list (ValentMprisPlugin *self)
490 : : {
491 : 10 : g_autoptr (JsonBuilder) builder = NULL;
492 [ - + ]: 5 : g_autoptr (JsonNode) packet = NULL;
493 : 5 : unsigned int n_players = 0;
494 : :
495 [ + - ]: 5 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
496 : :
497 : 5 : valent_packet_init (&builder, "kdeconnect.mpris");
498 : :
499 : : /* Player List */
500 : 5 : json_builder_set_member_name (builder, "playerList");
501 : 5 : json_builder_begin_array (builder);
502 : :
503 : 5 : n_players = g_list_model_get_n_items (G_LIST_MODEL (self->media));
504 : :
505 [ + + ]: 6 : for (unsigned int i = 0; i < n_players; i++)
506 : : {
507 : 1 : g_autoptr (ValentMediaPlayer) player = NULL;
508 : 1 : const char *name;
509 : :
510 : 1 : player = g_list_model_get_item (G_LIST_MODEL (self->media), i);
511 : :
512 [ - + ]: 1 : if (VALENT_IS_MPRIS_DEVICE (player))
513 [ # # ]: 0 : continue;
514 : :
515 : 1 : name = valent_media_player_get_name (player);
516 : :
517 [ + - ]: 1 : if (name != NULL)
518 : 1 : json_builder_add_string_value (builder, name);
519 : : }
520 : :
521 : 5 : json_builder_end_array (builder);
522 : :
523 : : /* Indicate that the remote device may send us album art payloads */
524 : 5 : json_builder_set_member_name (builder, "supportAlbumArtPayload");
525 : 5 : json_builder_add_boolean_value (builder, TRUE);
526 : :
527 : 5 : packet = valent_packet_end (&builder);
528 [ + - ]: 5 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
529 : 5 : }
530 : :
531 : : static void
532 : 14 : valent_mpris_plugin_watch_media (ValentMprisPlugin *self,
533 : : gboolean state)
534 : : {
535 [ + + ]: 14 : if (self->media_watch == state)
536 : : return;
537 : :
538 : 6 : self->media = valent_media_get_default ();
539 : :
540 [ + + ]: 6 : if (state)
541 : : {
542 : 3 : unsigned int n_players = 0;
543 : :
544 : 3 : n_players = g_list_model_get_n_items (G_LIST_MODEL (self->media));
545 : :
546 [ + + ]: 4 : for (unsigned int i = 0; i < n_players; i++)
547 : : {
548 : 1 : g_autoptr (ValentMediaPlayer) player = NULL;
549 : :
550 : 1 : player = g_list_model_get_item (G_LIST_MODEL (self->media), i);
551 [ + - ]: 1 : g_signal_connect_object (player,
552 : : "notify",
553 : : G_CALLBACK (on_player_changed),
554 : : self, 0);
555 : : }
556 : :
557 : 3 : g_signal_connect_object (self->media,
558 : : "items-changed",
559 : : G_CALLBACK (on_players_changed),
560 : : self, 0);
561 : :
562 : 3 : self->media_watch = TRUE;
563 : : }
564 : : else
565 : : {
566 : 3 : unsigned int n_players = 0;
567 : :
568 : 3 : n_players = g_list_model_get_n_items (G_LIST_MODEL (self->media));
569 : :
570 [ - + ]: 3 : for (unsigned int i = 0; i < n_players; i++)
571 : : {
572 : 0 : g_autoptr (ValentMediaPlayer) player = NULL;
573 : :
574 : 0 : player = g_list_model_get_item (G_LIST_MODEL (self->media), i);
575 [ # # ]: 0 : g_signal_handlers_disconnect_by_data (player, self);
576 : : }
577 : :
578 [ - + ]: 3 : g_clear_handle_id (&self->flush_id, g_source_remove);
579 : 3 : g_signal_handlers_disconnect_by_data (self->media, self);
580 : 3 : self->media_watch = FALSE;
581 : : }
582 : : }
583 : :
584 : : /*
585 : : * Remote Players
586 : : */
587 : : static void
588 : 1 : _valent_mpris_device_free (gpointer player)
589 : : {
590 : 1 : valent_media_unexport_player (valent_media_get_default (), player);
591 : 1 : g_object_unref (player);
592 : 1 : }
593 : :
594 : : static gboolean
595 : 4 : valent_mpris_plugin_find_player (ValentMprisPlugin *self,
596 : : const char *name,
597 : : ValentMediaPlayer **player)
598 : : {
599 [ + - ]: 4 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
600 [ + - - + ]: 4 : g_assert (name != NULL && *name != '\0');
601 : :
602 [ + - ]: 4 : for (unsigned int i = 0, len = self->players->len; i < len; i++)
603 : : {
604 : 4 : *player = g_ptr_array_index (self->players, i);
605 : :
606 [ - + ]: 4 : if (g_strcmp0 (valent_media_player_get_name (*player), name) == 0)
607 : : return TRUE;
608 : :
609 : 0 : *player = NULL;
610 : : }
611 : :
612 : : return FALSE;
613 : : }
614 : :
615 : : static void
616 : 3 : valent_mpris_plugin_request_player_list (ValentMprisPlugin *self)
617 : : {
618 : 6 : g_autoptr (JsonBuilder) builder = NULL;
619 [ - + ]: 3 : g_autoptr (JsonNode) packet = NULL;
620 : :
621 : 3 : valent_packet_init (&builder, "kdeconnect.mpris.request");
622 : 3 : json_builder_set_member_name (builder, "requestPlayerList");
623 : 3 : json_builder_add_boolean_value (builder, TRUE);
624 : 3 : packet = valent_packet_end (&builder);
625 : :
626 [ + - ]: 3 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
627 : 3 : }
628 : :
629 : : static void
630 : 1 : receive_art_cb (ValentTransfer *transfer,
631 : : GAsyncResult *result,
632 : : ValentMprisPlugin *self)
633 : : {
634 : 1 : g_autoptr (JsonNode) packet = NULL;
635 [ - - + - ]: 1 : g_autoptr (GFile) file = NULL;
636 [ - - + - ]: 1 : g_autoptr (GError) error = NULL;
637 : 1 : ValentMediaPlayer *player = NULL;
638 : 1 : const char *name;
639 : :
640 [ - + ]: 1 : if (!valent_transfer_execute_finish (transfer, result, &error))
641 : : {
642 [ # # ]: 0 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
643 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
644 : :
645 [ # # ]: 0 : return;
646 : : }
647 : :
648 : 1 : g_object_get (transfer,
649 : : "file", &file,
650 : : "packet", &packet,
651 : : NULL);
652 : :
653 [ + - + - ]: 2 : if (valent_packet_get_string (packet, "player", &name) &&
654 : 1 : valent_mpris_plugin_find_player (self, name, &player))
655 : 1 : valent_mpris_device_update_art (VALENT_MPRIS_DEVICE (player), file);
656 : : }
657 : :
658 : : static void
659 : 1 : valent_mpris_plugin_receive_album_art (ValentMprisPlugin *self,
660 : : JsonNode *packet)
661 : : {
662 : 1 : ValentDevice *device;
663 : 1 : ValentContext *context = NULL;
664 : 1 : const char *url;
665 : 1 : g_autofree char *filename = NULL;
666 : 1 : g_autoptr (GFile) file = NULL;
667 [ + - ]: 1 : g_autoptr (ValentTransfer) transfer = NULL;
668 : :
669 [ - + ]: 1 : if (!valent_packet_get_string (packet, "albumArtUrl", &url))
670 : : {
671 : 0 : g_debug ("%s(): expected \"albumArtUrl\" field holding a string",
672 : : G_STRFUNC);
673 : 0 : return;
674 : : }
675 : :
676 : 1 : device = valent_extension_get_object (VALENT_EXTENSION (self));
677 : 1 : context = valent_device_get_context (device);
678 : 1 : filename = g_compute_checksum_for_string (G_CHECKSUM_MD5, url, -1);
679 : 1 : file = valent_context_get_cache_file (context, filename);
680 : :
681 : 1 : transfer = valent_device_transfer_new (device, packet, file);
682 [ + - ]: 1 : valent_transfer_execute (transfer,
683 : : NULL,
684 : : (GAsyncReadyCallback)receive_art_cb,
685 : : self);
686 : : }
687 : :
688 : : static void
689 : 1 : valent_mpris_plugin_request_update (ValentMprisPlugin *self,
690 : : const char *player)
691 : : {
692 : 2 : g_autoptr (JsonBuilder) builder = NULL;
693 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
694 : :
695 : 1 : valent_packet_init (&builder, "kdeconnect.mpris.request");
696 : 1 : json_builder_set_member_name (builder, "player");
697 : 1 : json_builder_add_string_value (builder, player);
698 : 1 : json_builder_set_member_name (builder, "requestNowPlaying");
699 : 1 : json_builder_add_boolean_value (builder, TRUE);
700 : 1 : json_builder_set_member_name (builder, "requestVolume");
701 : 1 : json_builder_add_boolean_value (builder, TRUE);
702 : 1 : packet = valent_packet_end (&builder);
703 : :
704 [ + - ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
705 : 1 : }
706 : :
707 : : static void
708 : 2 : valent_mpris_plugin_handle_player_list (ValentMprisPlugin *self,
709 : : JsonArray *player_list)
710 : : {
711 : 2 : unsigned int n_remote, n_local, n_extant = 0;
712 : 4 : g_autofree const char **remote_names = NULL;
713 : 2 : g_autofree const char **local_names = NULL;
714 : 2 : ValentDevice *device = NULL;
715 : :
716 [ + - ]: 2 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
717 [ - + ]: 2 : g_assert (player_list != NULL);
718 : :
719 : : #ifndef __clang_analyzer__
720 : : /* Collect the remote player names */
721 : 2 : n_remote = json_array_get_length (player_list);
722 [ - + ]: 2 : remote_names = g_new0 (const char *, n_remote + 1);
723 : :
724 [ + + ]: 3 : for (unsigned int i = 0; i < n_remote; i++)
725 : 1 : remote_names[i] = json_array_get_string_element (player_list, i);
726 : :
727 : : /* Remove old players */
728 : 2 : n_local = self->players->len;
729 [ - + ]: 2 : local_names = g_new0 (const char *, n_local + 1);
730 : :
731 [ + + ]: 3 : for (unsigned int i = n_local; i-- > 0;)
732 : : {
733 : 1 : ValentMediaPlayer *export = g_ptr_array_index (self->players, i);
734 : 1 : const char *name = valent_media_player_get_name (export);
735 : :
736 [ - + ]: 1 : if (g_strv_contains (remote_names, name))
737 : : {
738 : 0 : local_names[n_extant++] = name;
739 : 0 : continue;
740 : : }
741 : :
742 : 1 : g_ptr_array_remove_index (self->players, i);
743 : : }
744 : :
745 : : /* Add new players */
746 : 2 : device = valent_extension_get_object (VALENT_EXTENSION (self));
747 : :
748 [ + + ]: 3 : for (unsigned int i = 0; remote_names[i] != NULL; i++)
749 : : {
750 : 1 : g_autoptr (ValentMprisDevice) player = NULL;
751 : 1 : const char *name = remote_names[i];
752 : :
753 [ - + ]: 1 : if (g_strv_contains (local_names, name))
754 : 0 : continue;
755 : :
756 : 1 : player = valent_mpris_device_new (device);
757 : 1 : valent_mpris_device_update_name (player, name);
758 : :
759 : 1 : g_ptr_array_add (self->players, g_object_ref (player));
760 : 1 : valent_media_export_player (self->media, VALENT_MEDIA_PLAYER (player));
761 : :
762 [ + - ]: 1 : valent_mpris_plugin_request_update (self, name);
763 : : }
764 : : #endif /* __clang_analyzer__ */
765 : 2 : }
766 : :
767 : : static void
768 : 3 : valent_mpris_plugin_handle_player_update (ValentMprisPlugin *self,
769 : : JsonNode *packet)
770 : : {
771 : 3 : ValentMediaPlayer *player = NULL;
772 : 3 : const char *name;
773 : :
774 : : /* Get the remote */
775 [ + - - + ]: 6 : if (!valent_packet_get_string (packet, "player", &name) ||
776 : 3 : !valent_mpris_plugin_find_player (self, name, &player))
777 : : {
778 : 0 : valent_mpris_plugin_request_player_list (self);
779 : 1 : return;
780 : : }
781 : :
782 [ + + ]: 3 : if (valent_packet_check_field (packet, "transferringAlbumArt"))
783 : : {
784 : 1 : valent_mpris_plugin_receive_album_art (self, packet);
785 : 1 : return;
786 : : }
787 : :
788 : 2 : valent_mpris_device_handle_packet (VALENT_MPRIS_DEVICE (player), packet);
789 : : }
790 : :
791 : : static void
792 : 5 : valent_mpris_plugin_handle_mpris (ValentMprisPlugin *self,
793 : : JsonNode *packet)
794 : : {
795 : 5 : JsonArray *player_list;
796 : :
797 [ + - ]: 5 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
798 [ - + ]: 5 : g_assert (VALENT_IS_PACKET (packet));
799 : :
800 [ + + ]: 5 : if (valent_packet_get_array (packet, "playerList", &player_list))
801 : 2 : valent_mpris_plugin_handle_player_list (self, player_list);
802 : :
803 [ + - ]: 3 : else if (valent_packet_get_string (packet, "player", NULL))
804 : 3 : valent_mpris_plugin_handle_player_update (self, packet);
805 : 5 : }
806 : :
807 : :
808 : : /*
809 : : * ValentDevicePlugin
810 : : */
811 : : static void
812 : 10 : valent_mpris_plugin_update_state (ValentDevicePlugin *plugin,
813 : : ValentDeviceState state)
814 : : {
815 : 10 : ValentMprisPlugin *self = VALENT_MPRIS_PLUGIN (plugin);
816 : 10 : gboolean available;
817 : :
818 [ + - ]: 10 : g_assert (VALENT_IS_MPRIS_PLUGIN (self));
819 : :
820 : 10 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
821 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
822 : :
823 [ + + ]: 10 : if (available)
824 : : {
825 : 3 : valent_mpris_plugin_watch_media (self, TRUE);
826 : 3 : valent_mpris_plugin_request_player_list (self);
827 : 3 : valent_mpris_plugin_send_player_list (self);
828 : : }
829 : : else
830 : : {
831 : 7 : valent_mpris_plugin_watch_media (self, FALSE);
832 : 7 : g_ptr_array_remove_range (self->players, 0, self->players->len);
833 : : }
834 : 10 : }
835 : :
836 : : static void
837 : 16 : valent_mpris_plugin_handle_packet (ValentDevicePlugin *plugin,
838 : : const char *type,
839 : : JsonNode *packet)
840 : : {
841 : 16 : ValentMprisPlugin *self = VALENT_MPRIS_PLUGIN (plugin);
842 : :
843 [ + - ]: 16 : g_assert (VALENT_IS_MPRIS_PLUGIN (plugin));
844 [ - + ]: 16 : g_assert (type != NULL);
845 [ - + ]: 16 : g_assert (VALENT_IS_PACKET (packet));
846 : :
847 [ + + ]: 16 : if (g_str_equal (type, "kdeconnect.mpris"))
848 : 5 : valent_mpris_plugin_handle_mpris (self, packet);
849 : :
850 [ + - ]: 11 : else if (g_str_equal (type, "kdeconnect.mpris.request"))
851 : 11 : valent_mpris_plugin_handle_mpris_request (self, packet);
852 : :
853 : : else
854 : 0 : g_assert_not_reached ();
855 : 16 : }
856 : :
857 : : /*
858 : : * ValentObject
859 : : */
860 : : static void
861 : 4 : valent_mpris_plugin_destroy (ValentObject *object)
862 : : {
863 : 4 : ValentMprisPlugin *self = VALENT_MPRIS_PLUGIN (object);
864 : :
865 : 4 : valent_mpris_plugin_watch_media (self, FALSE);
866 : 4 : self->media = NULL;
867 : :
868 : 4 : VALENT_OBJECT_CLASS (valent_mpris_plugin_parent_class)->destroy (object);
869 : 4 : }
870 : :
871 : : /*
872 : : * GObject
873 : : */
874 : : static void
875 : 3 : valent_mpris_plugin_constructed (GObject *object)
876 : : {
877 : 3 : ValentMprisPlugin *self = VALENT_MPRIS_PLUGIN (object);
878 : :
879 : 3 : self->media = valent_media_get_default ();
880 : :
881 : 3 : G_OBJECT_CLASS (valent_mpris_plugin_parent_class)->constructed (object);
882 : 3 : }
883 : :
884 : : static void
885 : 2 : valent_mpris_plugin_finalize (GObject *object)
886 : : {
887 : 2 : ValentMprisPlugin *self = VALENT_MPRIS_PLUGIN (object);
888 : :
889 [ + - ]: 2 : g_clear_pointer (&self->pending, g_hash_table_unref);
890 [ + - ]: 2 : g_clear_pointer (&self->players, g_ptr_array_unref);
891 [ + - ]: 2 : g_clear_pointer (&self->transfers, g_hash_table_unref);
892 : :
893 : 2 : G_OBJECT_CLASS (valent_mpris_plugin_parent_class)->finalize (object);
894 : 2 : }
895 : :
896 : : static void
897 : 2 : valent_mpris_plugin_class_init (ValentMprisPluginClass *klass)
898 : : {
899 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
900 : 2 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
901 : 2 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
902 : :
903 : 2 : object_class->constructed = valent_mpris_plugin_constructed;
904 : 2 : object_class->finalize = valent_mpris_plugin_finalize;
905 : :
906 : 2 : vobject_class->destroy = valent_mpris_plugin_destroy;
907 : :
908 : 2 : plugin_class->handle_packet = valent_mpris_plugin_handle_packet;
909 : 2 : plugin_class->update_state = valent_mpris_plugin_update_state;
910 : : }
911 : :
912 : : static void
913 : 3 : valent_mpris_plugin_init (ValentMprisPlugin *self)
914 : : {
915 : 3 : self->players = g_ptr_array_new_with_free_func (_valent_mpris_device_free);
916 : 3 : self->transfers = g_hash_table_new_full (g_str_hash,
917 : : g_str_equal,
918 : : g_free,
919 : : g_object_unref);
920 : 3 : self->pending = g_hash_table_new (NULL, NULL);
921 : 3 : }
922 : :
|