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 "vdp-mpris-adapter"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <valent.h>
10 : :
11 : : #include "vdp-mpris-player.h"
12 : :
13 : : #include "vdp-mpris-adapter.h"
14 : :
15 : : struct _VdpMprisAdapter
16 : : {
17 : : ValentMediaAdapter parent_instance;
18 : :
19 : : ValentDevice *device;
20 : : GCancellable *cancellable;
21 : : GHashTable *players;
22 : : GHashTable *transfers;
23 : : };
24 : :
25 [ + + + - ]: 19 : G_DEFINE_FINAL_TYPE (VdpMprisAdapter, vdp_mpris_adapter, VALENT_TYPE_MEDIA_ADAPTER)
26 : :
27 : : static inline void
28 : 4 : valent_device_send_packet_cb (ValentDevice *device,
29 : : GAsyncResult *result,
30 : : gpointer user_data)
31 : : {
32 : 8 : g_autoptr (GError) error = NULL;
33 : :
34 [ - + ]: 4 : if (!valent_device_send_packet_finish (device, result, &error))
35 : : {
36 [ # # ]: 0 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
37 : 0 : g_critical ("%s(): %s", G_STRFUNC, error->message);
38 [ # # ]: 0 : else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_CONNECTED))
39 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
40 [ # # ]: 0 : else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
41 : 0 : g_debug ("%s(): %s", G_STRFUNC, error->message);
42 : : }
43 : 4 : }
44 : :
45 : : static void
46 : 1 : receive_art_cb (ValentTransfer *transfer,
47 : : GAsyncResult *result,
48 : : VdpMprisAdapter *self)
49 : : {
50 : 1 : g_autoptr (JsonNode) packet = NULL;
51 [ - - + - ]: 1 : g_autoptr (GFile) file = NULL;
52 [ - - + - ]: 1 : g_autoptr (GError) error = NULL;
53 : 1 : ValentMediaPlayer *player = NULL;
54 : 1 : const char *name;
55 : :
56 [ - + ]: 1 : if (!valent_transfer_execute_finish (transfer, result, &error))
57 : : {
58 [ # # ]: 0 : if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
59 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
60 : :
61 [ # # ]: 0 : return;
62 : : }
63 : :
64 : 1 : g_object_get (transfer,
65 : : "file", &file,
66 : : "packet", &packet,
67 : : NULL);
68 : :
69 [ + - ]: 1 : if (valent_packet_get_string (packet, "player", &name))
70 : 1 : player = g_hash_table_lookup (self->players, name);;
71 : :
72 [ + - ]: 1 : if (player != NULL)
73 : 1 : vdp_mpris_player_update_art (VDP_MPRIS_PLAYER (player), file);
74 : : }
75 : :
76 : : static void
77 : 1 : vdp_mpris_adapter_receive_album_art (VdpMprisAdapter *self,
78 : : JsonNode *packet)
79 : : {
80 : 1 : ValentContext *context = NULL;
81 : 1 : const char *url;
82 : 1 : g_autofree char *filename = NULL;
83 : 1 : g_autoptr (GFile) file = NULL;
84 [ + - ]: 1 : g_autoptr (ValentTransfer) transfer = NULL;
85 : :
86 [ - + ]: 1 : if (!valent_packet_get_string (packet, "albumArtUrl", &url))
87 : : {
88 : 0 : g_debug ("%s(): expected \"albumArtUrl\" field holding a string",
89 : : G_STRFUNC);
90 : 0 : return;
91 : : }
92 : :
93 : 1 : context = valent_device_get_context (self->device);
94 : 1 : filename = g_compute_checksum_for_string (G_CHECKSUM_MD5, url, -1);
95 : 1 : file = valent_context_get_cache_file (context, filename);
96 : :
97 : 1 : transfer = valent_device_transfer_new (self->device, packet, file);
98 [ + - ]: 1 : valent_transfer_execute (transfer,
99 : : self->cancellable,
100 : : (GAsyncReadyCallback)receive_art_cb,
101 : : self);
102 : : }
103 : :
104 : : /*< private >
105 : : * @self: a `VdpMprisAdapter`
106 : : * @player: a
107 : : *
108 : : * Send a request for messages starting at @range_start_timestamp in
109 : : * oldest-to-newest order, for a maximum of @number_to_request.
110 : : */
111 : : static void
112 : 3 : vdp_mpris_adapter_request_player_list (VdpMprisAdapter *self)
113 : : {
114 : 6 : g_autoptr (JsonBuilder) builder = NULL;
115 [ - + ]: 3 : g_autoptr (JsonNode) packet = NULL;
116 : :
117 : 3 : valent_packet_init (&builder, "kdeconnect.mpris.request");
118 : 3 : json_builder_set_member_name (builder, "requestPlayerList");
119 : 3 : json_builder_add_boolean_value (builder, TRUE);
120 : 3 : packet = valent_packet_end (&builder);
121 : :
122 [ + - ]: 3 : valent_device_send_packet (self->device,
123 : : packet,
124 : : self->cancellable,
125 : : (GAsyncReadyCallback) valent_device_send_packet_cb,
126 : : NULL);
127 : 3 : }
128 : :
129 : : /*< private >
130 : : * @self: a `VdpMprisAdapter`
131 : : * @player: a player name
132 : : *
133 : : * Request an update for for @player.
134 : : */
135 : : static void
136 : 1 : vdp_mpris_adapter_request_update (VdpMprisAdapter *self,
137 : : const char *player)
138 : : {
139 : 2 : g_autoptr (JsonBuilder) builder = NULL;
140 [ - + ]: 1 : g_autoptr (JsonNode) packet = NULL;
141 : :
142 [ + - ]: 1 : g_assert (VDP_IS_MPRIS_ADAPTER (self));
143 [ - + ]: 1 : g_assert (player != NULL);
144 : :
145 : 1 : valent_packet_init (&builder, "kdeconnect.mpris.request");
146 : 1 : json_builder_set_member_name (builder, "player");
147 : 1 : json_builder_add_string_value (builder, player);
148 : 1 : json_builder_set_member_name (builder, "requestNowPlaying");
149 : 1 : json_builder_add_boolean_value (builder, TRUE);
150 : 1 : json_builder_set_member_name (builder, "requestVolume");
151 : 1 : json_builder_add_boolean_value (builder, TRUE);
152 : 1 : packet = valent_packet_end (&builder);
153 : :
154 [ + - ]: 1 : valent_device_send_packet (self->device,
155 : : packet,
156 : : self->cancellable,
157 : : (GAsyncReadyCallback) valent_device_send_packet_cb,
158 : : NULL);
159 : 1 : }
160 : :
161 : : /*
162 : : * ValentMediaAdapter
163 : : */
164 : : static void
165 : 6 : on_device_state_changed (ValentDevice *device,
166 : : GParamSpec *pspec,
167 : : VdpMprisAdapter *self)
168 : : {
169 : 6 : ValentDeviceState state = VALENT_DEVICE_STATE_NONE;
170 : 6 : gboolean available;
171 : :
172 : 6 : state = valent_device_get_state (device);
173 : 6 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
174 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
175 : :
176 [ + - + + ]: 6 : if (available && self->cancellable == NULL)
177 : : {
178 : 9 : g_autoptr (GCancellable) cancellable = NULL;
179 : :
180 : 3 : cancellable = g_cancellable_new ();
181 : 3 : self->cancellable = valent_object_chain_cancellable (VALENT_OBJECT (self),
182 : : cancellable);
183 [ + - ]: 3 : vdp_mpris_adapter_request_player_list (self);
184 : : }
185 [ # # ]: 0 : else if (!available && self->cancellable != NULL)
186 : : {
187 : 0 : g_hash_table_remove_all (self->players);
188 : 0 : g_cancellable_cancel (self->cancellable);
189 [ # # ]: 0 : g_clear_object (&self->cancellable);
190 : : }
191 : 6 : }
192 : :
193 : : /*
194 : : * GObject
195 : : */
196 : : static void
197 : 3 : vdp_mpris_adapter_constructed (GObject *object)
198 : : {
199 : 3 : VdpMprisAdapter *self = VDP_MPRIS_ADAPTER (object);
200 : :
201 : 3 : G_OBJECT_CLASS (vdp_mpris_adapter_parent_class)->constructed (object);
202 : :
203 : 3 : self->device = valent_resource_get_source (VALENT_RESOURCE (self));
204 : 3 : g_signal_connect_object (self->device,
205 : : "notify::state",
206 : : G_CALLBACK (on_device_state_changed),
207 : : self,
208 : : G_CONNECT_DEFAULT);
209 : 3 : on_device_state_changed (self->device, NULL, self);
210 : 3 : }
211 : :
212 : : static void
213 : 3 : vdp_mpris_adapter_finalize (GObject *object)
214 : : {
215 : 3 : VdpMprisAdapter *self = VDP_MPRIS_ADAPTER (object);
216 : :
217 [ + - ]: 3 : g_clear_object (&self->cancellable);
218 [ + - ]: 3 : g_clear_pointer (&self->players, g_hash_table_unref);
219 [ + - ]: 3 : g_clear_pointer (&self->transfers, g_hash_table_unref);
220 : :
221 : 3 : G_OBJECT_CLASS (vdp_mpris_adapter_parent_class)->finalize (object);
222 : 3 : }
223 : :
224 : : static void
225 : 1 : vdp_mpris_adapter_class_init (VdpMprisAdapterClass *klass)
226 : : {
227 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
228 : :
229 : 1 : object_class->constructed = vdp_mpris_adapter_constructed;
230 : 1 : object_class->finalize = vdp_mpris_adapter_finalize;
231 : : }
232 : :
233 : : static void
234 : 3 : vdp_mpris_adapter_init (VdpMprisAdapter *self)
235 : : {
236 : 3 : self->players = g_hash_table_new_full (g_str_hash,
237 : : g_str_equal,
238 : : g_free,
239 : : g_object_unref);
240 : 3 : self->transfers = g_hash_table_new_full (g_str_hash,
241 : : g_str_equal,
242 : : g_free,
243 : : g_object_unref);
244 : 3 : }
245 : :
246 : : /**
247 : : * vdp_mpris_adapter_new:
248 : : * @device: a `ValentDevice`
249 : : *
250 : : * Create a new `VdpMprisAdapter`.
251 : : *
252 : : * Returns: (transfer full): a new message store
253 : : */
254 : : ValentMediaAdapter *
255 : 3 : vdp_mpris_adapter_new (ValentDevice *device)
256 : : {
257 : 6 : g_autoptr (ValentContext) context = NULL;
258 [ + - ]: 3 : g_autofree char *iri = NULL;
259 : :
260 [ + - ]: 3 : g_return_val_if_fail (VALENT_IS_DEVICE (device), NULL);
261 : :
262 : 3 : context = valent_context_new (valent_device_get_context (device),
263 : : "plugin",
264 : : "mpris");
265 : 3 : iri = tracker_sparql_escape_uri_printf ("urn:valent:messages:%s",
266 : : valent_device_get_id (device));
267 : 3 : return g_object_new (VDP_TYPE_MPRIS_ADAPTER,
268 : : "iri", iri,
269 : : "context", context,
270 : : "source", device,
271 : : "title", valent_device_get_name (device),
272 : : NULL);
273 : : }
274 : :
275 : : static void
276 : 2 : vdp_mpris_adapter_handle_player_list (VdpMprisAdapter *self,
277 : : JsonArray *player_list)
278 : : {
279 : 2 : GHashTableIter iter;
280 : 2 : VdpMprisPlayer *player;
281 : 2 : const char *name;
282 : 2 : unsigned int n_remote = 0;
283 : 4 : g_autofree const char **remote_names = NULL;
284 : :
285 [ + - ]: 2 : g_assert (VDP_IS_MPRIS_ADAPTER (self));
286 [ - + ]: 2 : g_assert (player_list != NULL);
287 : :
288 : : #ifndef __clang_analyzer__
289 : : /* Collect the remote player names */
290 : 2 : n_remote = json_array_get_length (player_list);
291 [ - + ]: 2 : remote_names = g_new0 (const char *, n_remote + 1);
292 : :
293 [ + + ]: 3 : for (unsigned int i = 0; i < n_remote; i++)
294 : 1 : remote_names[i] = json_array_get_string_element (player_list, i);
295 : :
296 : : /* Remove old players */
297 : 2 : g_hash_table_iter_init (&iter, self->players);
298 [ + + ]: 5 : while (g_hash_table_iter_next (&iter, (void **)&name, (void **)&player))
299 : : {
300 [ + - ]: 1 : if (!g_strv_contains (remote_names, name))
301 : : {
302 : 1 : g_hash_table_iter_remove (&iter);
303 : 1 : valent_object_destroy (VALENT_OBJECT (player));
304 : : }
305 : : }
306 : :
307 : : /* Add new players */
308 [ + + ]: 3 : for (unsigned int i = 0; remote_names[i] != NULL; i++)
309 : : {
310 : 1 : name = remote_names[i];
311 [ - + ]: 1 : if (g_hash_table_contains (self->players, name))
312 : 0 : continue;
313 : :
314 : 1 : player = vdp_mpris_player_new (self->device);
315 : 1 : vdp_mpris_player_update_name (player, name);
316 : :
317 : 1 : valent_media_adapter_player_added (VALENT_MEDIA_ADAPTER (self),
318 : : VALENT_MEDIA_PLAYER (player));
319 : 1 : valent_media_export_player (valent_media_get_default (),
320 : : VALENT_MEDIA_PLAYER (player));
321 [ - + ]: 1 : g_hash_table_insert (self->players,
322 : 1 : g_strdup (name),
323 : : g_steal_pointer (&player));
324 : :
325 : 1 : vdp_mpris_adapter_request_update (self, name);
326 : : }
327 : : #endif /* __clang_analyzer__ */
328 : 2 : }
329 : :
330 : : static void
331 : 3 : vdp_mpris_adapter_handle_player_update (VdpMprisAdapter *self,
332 : : JsonNode *packet)
333 : : {
334 : 3 : ValentMediaPlayer *player = NULL;
335 : 3 : const char *name;
336 : :
337 : : /* Get the remote */
338 [ + - ]: 3 : if (valent_packet_get_string (packet, "player", &name))
339 : 3 : player = g_hash_table_lookup (self->players, name);
340 : :
341 [ - + ]: 3 : if (player == NULL)
342 : : {
343 : 0 : vdp_mpris_adapter_request_player_list (self);
344 : 1 : return;
345 : : }
346 : :
347 [ + + ]: 3 : if (valent_packet_check_field (packet, "transferringAlbumArt"))
348 : : {
349 : 1 : vdp_mpris_adapter_receive_album_art (self, packet);
350 : 1 : return;
351 : : }
352 : :
353 : 2 : vdp_mpris_player_handle_packet (VDP_MPRIS_PLAYER (player), packet);
354 : : }
355 : :
356 : : /**
357 : : * vdp_mpris_adapter_handle_packet:
358 : : * @self: a `VdpMprisAdapter`
359 : : * @packet: a `kdeconnect.sms.attachment_file` packet
360 : : *
361 : : * Handle an attachment file.
362 : : */
363 : : void
364 : 5 : vdp_mpris_adapter_handle_packet (VdpMprisAdapter *self,
365 : : JsonNode *packet)
366 : : {
367 : 5 : JsonArray *player_list;
368 : :
369 [ + - ]: 5 : g_assert (VDP_IS_MPRIS_ADAPTER (self));
370 [ - + ]: 5 : g_assert (VALENT_IS_PACKET (packet));
371 : :
372 [ + + ]: 5 : if (valent_packet_get_array (packet, "playerList", &player_list))
373 : 2 : vdp_mpris_adapter_handle_player_list (self, player_list);
374 [ + - ]: 3 : else if (valent_packet_get_string (packet, "player", NULL))
375 : 3 : vdp_mpris_adapter_handle_player_update (self, packet);
376 : : else
377 : 0 : g_assert_not_reached ();
378 : 5 : }
379 : :
|