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