1 /* Implmentation of DACP (e.g., iTunes Remote) sharing
2 *
3 * Copyright (C) 2010 Alexandre Rosenfeld <airmind@gmail.com>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 *
19 */
20
21 #include "config.h"
22
23 #include <time.h>
24 #include <string.h>
25 #include <stdlib.h>
26
27 #include <glib.h>
28 #include <glib-object.h>
29
30 #ifdef HAVE_GDKPIXBUF
31 #include <gdk-pixbuf/gdk-pixbuf.h>
32 #endif /* HAVE_GDKPIXBUF */
33
34 #include <libsoup/soup.h>
35 #include <libsoup/soup-address.h>
36 #include <libsoup/soup-message.h>
37 #include <libsoup/soup-uri.h>
38 #include <libsoup/soup-server.h>
39
40 #include <libdmapsharing/dmap.h>
41 #include <libdmapsharing/dmap-structure.h>
42
43 #include <libdmapsharing/dmap-share.h>
44 #include <libdmapsharing/dacp-share.h>
45 #include <libdmapsharing/dacp-connection.h>
46 #include <libdmapsharing/dacp-player.h>
47
48 static void dacp_share_set_property (GObject * object,
49 guint prop_id,
50 const GValue * value,
51 GParamSpec * pspec);
52 static void dacp_share_get_property (GObject * object,
53 guint prop_id,
54 GValue * value, GParamSpec * pspec);
55 static void dacp_share_dispose (GObject * object);
56 const char *dacp_share_get_type_of_service (DMAPShare * share);
57 void dacp_share_ctrl_int (DMAPShare * share,
58 SoupServer * server,
59 SoupMessage * message,
60 const char *path,
61 GHashTable * query, SoupClientContext * context);
62 void dacp_share_login (DMAPShare * share,
63 SoupServer * server,
64 SoupMessage * message,
65 const char *path,
66 GHashTable * query, SoupClientContext * context);
67
68 static gchar *dacp_share_pairing_code (DACPShare * share, gchar * pair_txt,
69 gchar passcode[4]);
70 static void dacp_share_send_playstatusupdate (DACPShare * share);
71 static void dacp_share_fill_playstatusupdate (DACPShare * share,
72 SoupMessage * message);
73
74 #define DACP_TYPE_OF_SERVICE "_touch-able._tcp"
75 #define DACP_PORT 3689
76
77 struct DACPSharePrivate
78 {
79 DMAPMdnsBrowser *mdns_browser;
80
81 gchar *library_name;
82 GHashTable *remotes;
83
84 gint current_revision;
85
86 GSList *update_queue;
87
88 DACPPlayer *player;
89 };
90
91 /*
92 * Internal representation of a DACP remote.
93 */
94 typedef struct
95 {
96 gchar *host;
97 guint port;
98 gchar *pair_txt;
99 DMAPConnection *connection;
100 } DACPRemoteInfo;
101
102 enum
103 {
104 PROP_0,
105 PROP_LIBRARY_NAME,
106 PROP_PLAYER
107 };
108
109 enum
110 {
111 REMOTE_FOUND,
112 REMOTE_LOST,
113 REMOTE_PAIRED,
114
115 LOOKUP_GUID,
116 ADD_GUID,
117
118 LAST_SIGNAL
119 };
120
121 static guint signals[LAST_SIGNAL] = { 0, };
122
123 G_DEFINE_TYPE_WITH_PRIVATE (DACPShare, dacp_share, DAAP_TYPE_SHARE);
124
125 static void
dacp_share_class_init(DACPShareClass * klass)126 dacp_share_class_init (DACPShareClass * klass)
127 {
128 GObjectClass *object_class = G_OBJECT_CLASS (klass);
129 DMAPShareClass *dmap_class = DMAP_SHARE_CLASS (object_class);
130
131 object_class->get_property = dacp_share_get_property;
132 object_class->set_property = dacp_share_set_property;
133 object_class->dispose = dacp_share_dispose;
134
135 dmap_class->get_type_of_service = dacp_share_get_type_of_service;
136 dmap_class->ctrl_int = dacp_share_ctrl_int;
137 dmap_class->login = dacp_share_login;
138
139 g_object_class_install_property (object_class,
140 PROP_LIBRARY_NAME,
141 g_param_spec_string ("library-name",
142 "Library Name",
143 "Library name as will be shown in the Remote",
144 NULL,
145 G_PARAM_READWRITE));
146
147 g_object_class_install_property (object_class,
148 PROP_PLAYER,
149 g_param_spec_object ("player",
150 "Player",
151 "Player",
152 G_TYPE_OBJECT,
153 G_PARAM_READWRITE
154 |
155 G_PARAM_CONSTRUCT_ONLY));
156
157 /**
158 * DACPShare::remote-found
159 * @share: the #DACPShare that received the signal.
160 * @service_name: the remote identifier.
161 * @remote_name: the remote friendly name.
162 *
163 * Signal emited when a remote is found in the local network.
164 */
165 signals[REMOTE_FOUND] =
166 g_signal_new ("remote-found",
167 G_TYPE_FROM_CLASS (object_class),
168 G_SIGNAL_RUN_LAST,
169 G_STRUCT_OFFSET (DACPShareClass, remote_found),
170 NULL,
171 NULL,
172 NULL,
173 G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
174
175 /**
176 * DACPShare::remote-lost
177 * @share: the #DACPShare that received the signal
178 * @service_name: the remote identifier.
179 *
180 * Signal emited when a remote is lost in the local network.
181 */
182 signals[REMOTE_LOST] =
183 g_signal_new ("remote-lost",
184 G_TYPE_FROM_CLASS (object_class),
185 G_SIGNAL_RUN_LAST,
186 G_STRUCT_OFFSET (DACPShareClass, remote_lost),
187 NULL,
188 NULL,
189 NULL,
190 G_TYPE_NONE, 1, G_TYPE_STRING);
191
192 /**
193 * DACPShare::remote-paired
194 * @share: the #DACPShare that received the signal
195 * @service_name: the remote identifier.
196 * @connected: indicates if the connection was succesfull or not.
197 *
198 * Signal emited when a remote is paired.
199 */
200 signals[REMOTE_PAIRED] =
201 g_signal_new ("remote-paired",
202 G_TYPE_FROM_CLASS (object_class),
203 G_SIGNAL_RUN_LAST,
204 G_STRUCT_OFFSET (DACPShareClass, remote_paired),
205 NULL,
206 NULL,
207 NULL,
208 G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_BOOLEAN);
209
210 /**
211 * DACPShare::lookup-guid
212 * @share: the #DACPShare that received the signal
213 * @guid: a string containing the guid to be validated.
214 *
215 * Signal emited when the remote has logged in before and wants to be
216 * validated. An implementation must implement this signal to lookup
217 * for guids saved by ::add-guid
218 */
219 signals[LOOKUP_GUID] =
220 g_signal_new ("lookup-guid",
221 G_TYPE_FROM_CLASS (object_class),
222 G_SIGNAL_RUN_LAST,
223 G_STRUCT_OFFSET (DACPShareClass, lookup_guid),
224 NULL,
225 NULL,
226 NULL,
227 G_TYPE_BOOLEAN, 1, G_TYPE_STRING);
228
229 /**
230 * DACPShare::add-guid
231 * @share: the #DACPShare that received the signal
232 * @guid: a string containing the guid to be saved.
233 *
234 * Signal emited when the remote wants to log in and save a special guid
235 * which will be used later when it wants to reconnect. With this guid,
236 * we know that this remote has connected before, thus this signal must
237 * save somewhere all guids that connected before, so that ::lookup-guid
238 * will find this remote. The user interface probably wants to include
239 * a button to forget previously connected remotes, so that the user may
240 * disconnect all previously connected remotes.
241 */
242 signals[ADD_GUID] =
243 g_signal_new ("add-guid",
244 G_TYPE_FROM_CLASS (object_class),
245 G_SIGNAL_RUN_LAST,
246 G_STRUCT_OFFSET (DACPShareClass, add_guid),
247 NULL,
248 NULL,
249 NULL,
250 G_TYPE_NONE, 1, G_TYPE_STRING);
251 }
252
253 static void
dacp_share_init(DACPShare * share)254 dacp_share_init (DACPShare * share)
255 {
256 share->priv = dacp_share_get_instance_private(share);
257
258 share->priv->current_revision = 2;
259
260 share->priv->remotes = g_hash_table_new_full ((GHashFunc) g_str_hash,
261 (GEqualFunc)
262 g_str_equal,
263 (GDestroyNotify) g_free,
264 (GDestroyNotify)
265 g_free);
266 }
267
268 static gchar *
get_dbid(void)269 get_dbid (void)
270 {
271 static gchar *dbid;
272
273 if (!dbid) {
274 GString *name;
275
276 // Creates a service name 14 characters long concatenating the hostname
277 // hash hex value with itself.
278 // Idea taken from stereo.
279 name = g_string_new (NULL);
280 g_string_printf (name, "%.8x",
281 g_str_hash (g_get_host_name ()));
282 g_string_ascii_up (name);
283 g_string_append_len (name, name->str, 4);
284
285 dbid = name->str;
286
287 g_string_free (name, FALSE);
288 }
289 return dbid;
290 }
291
292 static void
dacp_share_update_txt_records(DACPShare * share)293 dacp_share_update_txt_records (DACPShare * share)
294 {
295 gchar *dbid_record;
296 gchar *library_name_record;
297
298 library_name_record =
299 g_strdup_printf ("CtlN=%s", share->priv->library_name);
300 dbid_record = g_strdup_printf ("DbId=%s", get_dbid ());
301
302 gchar *txt_records[] = { "Ver=131073",
303 "DvSv=2049",
304 dbid_record,
305 "DvTy=iTunes",
306 "OSsi=0x1F6",
307 "txtvers=1",
308 library_name_record,
309 NULL
310 };
311
312 g_object_set (share, "txt-records", txt_records, NULL);
313
314 g_free (dbid_record);
315 g_free (library_name_record);
316 }
317
318 static void
dacp_share_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)319 dacp_share_set_property (GObject * object,
320 guint prop_id,
321 const GValue * value, GParamSpec * pspec)
322 {
323 DACPShare *share = DACP_SHARE (object);
324
325 switch (prop_id) {
326 case PROP_LIBRARY_NAME:
327 g_free (share->priv->library_name);
328 share->priv->library_name = g_value_dup_string (value);
329 dacp_share_update_txt_records (share);
330 break;
331 case PROP_PLAYER:
332 if (share->priv->player)
333 g_object_unref (share->priv->player);
334 share->priv->player =
335 DACP_PLAYER (g_value_dup_object (value));
336 break;
337 default:
338 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
339 break;
340 }
341 }
342
343 static void
dacp_share_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)344 dacp_share_get_property (GObject * object,
345 guint prop_id, GValue * value, GParamSpec * pspec)
346 {
347 DACPShare *share = DACP_SHARE (object);
348
349 switch (prop_id) {
350 case PROP_LIBRARY_NAME:
351 g_value_set_string (value, share->priv->library_name);
352 break;
353 case PROP_PLAYER:
354 g_value_set_object (value, G_OBJECT (share->priv->player));
355 break;
356 default:
357 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
358 break;
359 }
360 }
361
362 static void
dacp_share_dispose(GObject * object)363 dacp_share_dispose (GObject * object)
364 {
365 DACPShare *share = DACP_SHARE (object);
366
367 g_free (share->priv->library_name);
368
369 if (share->priv->mdns_browser)
370 g_object_unref (share->priv->mdns_browser);
371
372 if (share->priv->player)
373 g_object_unref (share->priv->player);
374
375 g_slist_free (share->priv->update_queue);
376
377 g_hash_table_destroy (share->priv->remotes);
378 }
379
380 static void
mdns_remote_added(G_GNUC_UNUSED DMAPMdnsBrowser * browser,DMAPMdnsBrowserService * service,DACPShare * share)381 mdns_remote_added (G_GNUC_UNUSED DMAPMdnsBrowser * browser,
382 DMAPMdnsBrowserService * service, DACPShare * share)
383 {
384 DACPRemoteInfo *remote_info;
385
386 remote_info = g_new0 (DACPRemoteInfo, 1);
387 remote_info->host = g_strdup (service->host);
388 remote_info->port = service->port;
389 remote_info->pair_txt = g_strdup (service->pair);
390 remote_info->connection = NULL;
391
392 g_debug ("New Remote found: %s name=%s host=%s port=%u pair=%s",
393 service->service_name,
394 service->name,
395 remote_info->host, remote_info->port, remote_info->pair_txt);
396
397 g_hash_table_insert (share->priv->remotes,
398 service->service_name, remote_info);
399
400 g_signal_emit (share,
401 signals[REMOTE_FOUND],
402 0, service->service_name, service->name);
403 }
404
405 static void
mdns_remote_removed(G_GNUC_UNUSED DMAPMdnsBrowser * browser,const char * service_name,DACPShare * share)406 mdns_remote_removed (G_GNUC_UNUSED DMAPMdnsBrowser * browser,
407 const char *service_name, DACPShare * share)
408 {
409 g_signal_emit (share, signals[REMOTE_LOST], 0, service_name);
410
411 g_hash_table_remove (share->priv->remotes, service_name);
412 }
413
414 DACPShare *
dacp_share_new(const gchar * library_name,DACPPlayer * player,DMAPDb * db,DMAPContainerDb * container_db)415 dacp_share_new (const gchar * library_name,
416 DACPPlayer * player,
417 DMAPDb * db, DMAPContainerDb * container_db)
418 {
419 DACPShare *share;
420
421 g_object_ref (db);
422 g_object_ref (container_db);
423
424 share = DACP_SHARE (g_object_new (DACP_TYPE_SHARE,
425 "name", get_dbid (),
426 "library-name", library_name,
427 "password", NULL,
428 "db", db,
429 "container-db", container_db,
430 "player", G_OBJECT (player),
431 "transcode-mimetype", NULL, NULL));
432
433 g_debug ("Starting DACP server");
434 _dmap_share_server_start (DMAP_SHARE (share));
435 _dmap_share_publish_start (DMAP_SHARE (share));
436
437 return share;
438 }
439
440 void
dacp_share_start_lookup(DACPShare * share)441 dacp_share_start_lookup (DACPShare * share)
442 {
443 GError *error;
444
445 if (share->priv->mdns_browser) {
446 g_warning ("DACP browsing already started");
447 return;
448 }
449
450 share->priv->mdns_browser =
451 dmap_mdns_browser_new (DMAP_MDNS_BROWSER_SERVICE_TYPE_DACP);
452
453 g_signal_connect_object (share->priv->mdns_browser,
454 "service-added",
455 G_CALLBACK (mdns_remote_added), share, 0);
456 g_signal_connect_object (share->priv->mdns_browser,
457 "service-removed",
458 G_CALLBACK (mdns_remote_removed), share, 0);
459
460 error = NULL;
461 dmap_mdns_browser_start (share->priv->mdns_browser, &error);
462 if (error != NULL) {
463 g_warning ("Unable to start Remote lookup: %s",
464 error->message);
465 g_error_free (error);
466 }
467 }
468
469 static gboolean
remove_remotes_cb(gpointer service_name,G_GNUC_UNUSED gpointer remote_info,gpointer share)470 remove_remotes_cb (gpointer service_name, G_GNUC_UNUSED gpointer remote_info,
471 gpointer share)
472 {
473 g_signal_emit ((DACPShare *) share,
474 signals[REMOTE_LOST], 0, (gchar *) service_name);
475 return TRUE;
476 }
477
478 void
dacp_share_stop_lookup(DACPShare * share)479 dacp_share_stop_lookup (DACPShare * share)
480 {
481 GError *error;
482
483 if (!share->priv->mdns_browser) {
484 g_warning ("DACP browsing not started");
485 return;
486 }
487
488 g_hash_table_foreach_remove (share->priv->remotes, remove_remotes_cb,
489 share);
490
491 error = NULL;
492 dmap_mdns_browser_stop (share->priv->mdns_browser, &error);
493 if (error != NULL) {
494 g_warning ("Unable to stop Remote lookup: %s",
495 error->message);
496 g_error_free (error);
497 }
498
499 share->priv->mdns_browser = NULL;
500 }
501
502 const char *
dacp_share_get_type_of_service(G_GNUC_UNUSED DMAPShare * share)503 dacp_share_get_type_of_service (G_GNUC_UNUSED DMAPShare * share)
504 {
505 return DACP_TYPE_OF_SERVICE;
506 }
507
508 void
dacp_share_player_updated(DACPShare * share)509 dacp_share_player_updated (DACPShare * share)
510 {
511 share->priv->current_revision++;
512 dacp_share_send_playstatusupdate (share);
513 }
514
515 static void
status_update_message_finished(SoupMessage * message,DACPShare * share)516 status_update_message_finished (SoupMessage * message, DACPShare * share)
517 {
518 share->priv->update_queue =
519 g_slist_remove (share->priv->update_queue, message);
520 g_object_unref (message);
521 }
522
523 static void
dacp_share_send_playstatusupdate(DACPShare * share)524 dacp_share_send_playstatusupdate (DACPShare * share)
525 {
526 GSList *list;
527 SoupServer *server = NULL;
528
529 g_object_get (share, "server", &server, NULL);
530 if (server) {
531 for (list = share->priv->update_queue; list;
532 list = list->next) {
533 dacp_share_fill_playstatusupdate (share,
534 (SoupMessage*) list->data);
535 soup_server_unpause_message (server,
536 (SoupMessage*) list->data);
537 }
538 g_object_unref (server);
539 }
540 g_slist_free (share->priv->update_queue);
541 share->priv->update_queue = NULL;
542 }
543
544 static void
dacp_share_fill_playstatusupdate(DACPShare * share,SoupMessage * message)545 dacp_share_fill_playstatusupdate (DACPShare * share, SoupMessage * message)
546 {
547 GNode *cmst;
548 DAAPRecord *record;
549 DACPPlayState play_state;
550 DACPRepeatState repeat_state;
551 gboolean shuffle_state;
552 guint playing_time;
553
554 g_object_get (share->priv->player,
555 "play-state", &play_state,
556 "repeat-state", &repeat_state,
557 "shuffle-state", &shuffle_state,
558 "playing-time", &playing_time, NULL);
559
560 record = dacp_player_now_playing_record (share->priv->player);
561
562 cmst = dmap_structure_add (NULL, DMAP_CC_CMST);
563 dmap_structure_add (cmst, DMAP_CC_MSTT, (gint32) DMAP_STATUS_OK);
564 dmap_structure_add (cmst, DMAP_CC_CMSR,
565 share->priv->current_revision);
566 dmap_structure_add (cmst, DMAP_CC_CAVC, 1);
567 dmap_structure_add (cmst, DMAP_CC_CAPS, (gint32) play_state);
568 dmap_structure_add (cmst, DMAP_CC_CASH, shuffle_state ? 1 : 0);
569 dmap_structure_add (cmst, DMAP_CC_CARP, (gint32) repeat_state);
570 if (record) {
571 gchar *title;
572 gchar *artist;
573 gchar *album;
574 gint duration;
575 guint track_time;
576
577 g_object_get (record,
578 "title", &title,
579 "songartist", &artist,
580 "songalbum", &album,
581 "duration", &duration, NULL);
582 track_time = duration * 1000;
583 dmap_structure_add (cmst, DMAP_CC_CAAS, 2);
584 dmap_structure_add (cmst, DMAP_CC_CAAR, 6);
585 dmap_structure_add (cmst, DMAP_CC_CANP, (gint64) 0); // FIXME: may be wrong.
586 if (title)
587 dmap_structure_add (cmst, DMAP_CC_CANN, title);
588 if (artist)
589 dmap_structure_add (cmst, DMAP_CC_CANA, artist);
590 if (album)
591 dmap_structure_add (cmst, DMAP_CC_CANL, album);
592 dmap_structure_add (cmst, DMAP_CC_CANG, "");
593 dmap_structure_add (cmst, DMAP_CC_ASAI, 0);
594 //dmap_structure_add (cmst, DMAP_CC_AEMK, 1);
595 g_debug ("Playing time: %u, Track time: %u", playing_time,
596 track_time);
597 dmap_structure_add (cmst, DMAP_CC_CANT,
598 track_time - playing_time);
599 dmap_structure_add (cmst, DMAP_CC_CAST, track_time);
600
601 g_free (title);
602 g_free (artist);
603 g_free (album);
604
605 g_object_unref (record);
606 }
607
608 _dmap_share_message_set_from_dmap_structure (DMAP_SHARE (share),
609 message, cmst);
610 dmap_structure_destroy (cmst);
611 }
612
613 static void
debug_param(gpointer key,gpointer val,G_GNUC_UNUSED gpointer user_data)614 debug_param (gpointer key, gpointer val, G_GNUC_UNUSED gpointer user_data)
615 {
616 g_debug ("%s %s", (char *) key, (char *) val);
617 }
618
619 void
dacp_share_login(DMAPShare * share,SoupServer * server,SoupMessage * message,const char * path,GHashTable * query,SoupClientContext * context)620 dacp_share_login (DMAPShare * share,
621 SoupServer * server,
622 SoupMessage * message,
623 const char *path,
624 GHashTable * query, SoupClientContext * context)
625 {
626 gchar *pairing_guid;
627
628 g_debug ("Path is %s.", path);
629 if (query) {
630 g_hash_table_foreach (query, debug_param, NULL);
631 }
632
633 pairing_guid = g_hash_table_lookup (query, "pairing-guid");
634
635 if (pairing_guid != NULL) {
636 gboolean allow_login;
637
638 g_signal_emit (share, signals[LOOKUP_GUID], 0, pairing_guid,
639 &allow_login);
640
641 if (!allow_login) {
642 g_debug ("Unknown remote trying to connect");
643 soup_message_set_status (message,
644 SOUP_STATUS_FORBIDDEN);
645 return;
646 }
647 }
648
649 _dmap_share_login (share, server, message, path, query, context);
650 }
651
652 void
dacp_share_ctrl_int(DMAPShare * share,SoupServer * server,SoupMessage * message,const char * path,GHashTable * query,SoupClientContext * context)653 dacp_share_ctrl_int (DMAPShare * share,
654 SoupServer * server,
655 SoupMessage * message,
656 const char *path,
657 GHashTable * query, SoupClientContext * context)
658 {
659 const char *rest_of_path;
660
661 DACPShare *dacp_share = DACP_SHARE (share);
662
663 g_debug ("Path is %s.", path);
664 if (query) {
665 g_hash_table_foreach (query, debug_param, NULL);
666 }
667
668 rest_of_path = strchr (path + 1, '/');
669
670 /* If calling /ctrl-int without args, the client doesnt need a
671 * session-id, otherwise it does and it should be validated. */
672 if ((rest_of_path != NULL)
673 &&
674 (!_dmap_share_session_id_validate
675 (share, context, message, query, NULL))) {
676 soup_message_set_status (message, SOUP_STATUS_FORBIDDEN);
677 return;
678 }
679
680 if (rest_of_path == NULL) {
681 /* CACI control-int
682 * MSTT status
683 * MUTY update type
684 * MTCO specified total count
685 * MRCO returned count
686 * MLCL listing
687 * MLIT listing item
688 * MIID item id
689 * CMIK Unknown (TRUE)
690 * CMSP Unknown (TRUE)
691 * CMSV Unknown (TRUE)
692 * CASS Unknown (TRUE)
693 * CASU Unknown (TRUE)
694 * CASG Unknown (TRUE)
695 */
696
697 GNode *caci;
698 GNode *mlcl;
699 GNode *mlit;
700
701 // dacp.controlint
702 caci = dmap_structure_add (NULL, DMAP_CC_CACI);
703 // dmap.status
704 dmap_structure_add (caci, DMAP_CC_MSTT,
705 (gint32) DMAP_STATUS_OK);
706 // dmap.updatetype
707 dmap_structure_add (caci, DMAP_CC_MUTY, 0);
708 // dmap.specifiedtotalcount
709 dmap_structure_add (caci, DMAP_CC_MTCO, (gint32) 1);
710 // dmap.returnedcount
711 dmap_structure_add (caci, DMAP_CC_MRCO, (gint32) 1);
712 // dmap.listing
713 mlcl = dmap_structure_add (caci, DMAP_CC_MLCL);
714 // dmap.listingitem
715 mlit = dmap_structure_add (mlcl, DMAP_CC_MLIT);
716 // dmap.itemid
717 dmap_structure_add (mlit, DMAP_CC_MIID, (gint32) 1);
718 // Unknown (TRUE)
719 dmap_structure_add (mlit, DMAP_CC_CMIK, (gint32) 1);
720 // Unknown (TRUE)
721 dmap_structure_add (mlit, DMAP_CC_CMPR, (gint32) (2 << 16 | 1));
722 dmap_structure_add (mlit, DMAP_CC_CAPR, (gint32) (2 << 16 | 2));
723 dmap_structure_add (mlit, DMAP_CC_CMSP, (gint32) 1);
724 // Unknown (TRUE)
725 dmap_structure_add (mlit, DMAP_CC_AEFR, 0x64);
726 dmap_structure_add (mlit, DMAP_CC_CMSV, (gint32) 1);
727 // Unknown (TRUE)
728 dmap_structure_add (mlit, DMAP_CC_CASS, (gint32) 1);
729 // Unknown (TRUE)
730 dmap_structure_add (mlit, DMAP_CC_CAOV, 1);
731 dmap_structure_add (mlit, DMAP_CC_CASU, (gint32) 1);
732 // Unknown (TRUE)
733 dmap_structure_add (mlit, DMAP_CC_CASG, (gint32) 1);
734 dmap_structure_add (mlit, DMAP_CC_CMRL, 1);
735
736 _dmap_share_message_set_from_dmap_structure (share, message,
737 caci);
738 dmap_structure_destroy (caci);
739 } else if (g_ascii_strcasecmp ("/1/getproperty", rest_of_path) == 0) {
740 gchar *properties_query, **properties, **property;
741 GNode *cmgt;
742
743 properties_query = g_hash_table_lookup (query, "properties");
744
745 if (!properties_query) {
746 g_warning ("No property specified");
747 return;
748 }
749
750 cmgt = dmap_structure_add (NULL, DMAP_CC_CMGT);
751 dmap_structure_add (cmgt, DMAP_CC_MSTT, DMAP_STATUS_OK);
752
753 properties = g_strsplit (properties_query, ",", -1);
754 for (property = properties; *property; property++) {
755 if (g_ascii_strcasecmp (*property, "dmcp.volume") ==
756 0) {
757 gulong volume;
758
759 g_object_get (dacp_share->priv->player,
760 "volume", &volume, NULL);
761 dmap_structure_add (cmgt, DMAP_CC_CMVO,
762 volume);
763 } else {
764 g_warning ("Unhandled property %s",
765 *property);
766 }
767 }
768
769 g_strfreev (properties);
770
771 _dmap_share_message_set_from_dmap_structure (share, message,
772 cmgt);
773 dmap_structure_destroy (cmgt);
774 } else if (g_ascii_strcasecmp ("/1/setproperty", rest_of_path) == 0) {
775 if (g_hash_table_lookup (query, "dmcp.volume")) {
776 gdouble volume =
777 strtod (g_hash_table_lookup
778 (query, "dmcp.volume"), NULL);
779 g_object_set (dacp_share->priv->player, "volume",
780 (gulong) volume, NULL);
781 }
782 soup_message_set_status (message, SOUP_STATUS_NO_CONTENT);
783 } else if (g_ascii_strcasecmp ("/1/getspeakers", rest_of_path) == 0) {
784 GNode *casp;
785 gulong volume;
786
787 casp = dmap_structure_add (NULL, DMAP_CC_CASP);
788 dmap_structure_add (casp, DMAP_CC_MSTT,
789 (gint32) DMAP_STATUS_OK);
790 dmap_structure_add (casp, DMAP_CC_MDCL);
791
792 dmap_structure_add (casp, DMAP_CC_CAIA, TRUE);
793 dmap_structure_add (casp, DMAP_CC_CAHP, 1);
794 dmap_structure_add (casp, DMAP_CC_CAIV, 1);
795 dmap_structure_add (casp, DMAP_CC_MINM, "Computer");
796 dmap_structure_add (casp, DMAP_CC_MSMA, (gint32) 0);
797
798 g_object_get (dacp_share->priv->player, "volume", &volume, NULL);
799 dmap_structure_add (casp, DMAP_CC_CMVO, volume);
800
801 _dmap_share_message_set_from_dmap_structure (share, message,
802 casp);
803 dmap_structure_destroy (casp);
804 } else if (g_ascii_strcasecmp ("/1/playstatusupdate", rest_of_path) ==
805 0) {
806 gchar *revision =
807 g_hash_table_lookup (query, "revision-number");
808 gint revision_number = atoi (revision);
809
810 if (revision_number >= dacp_share->priv->current_revision) {
811 g_object_ref (message);
812 dacp_share->priv->update_queue =
813 g_slist_prepend (dacp_share->
814 priv->update_queue, message);
815 g_signal_connect_object (message, "finished",
816 G_CALLBACK
817 (status_update_message_finished),
818 dacp_share, 0);
819 soup_server_pause_message (server, message);
820 } else {
821 dacp_share_fill_playstatusupdate (dacp_share,
822 message);
823 }
824 } else if (g_ascii_strcasecmp ("/1/playpause", rest_of_path) == 0) {
825 dacp_player_play_pause (dacp_share->priv->player);
826 soup_message_set_status (message, SOUP_STATUS_NO_CONTENT);
827 } else if (g_ascii_strcasecmp ("/1/pause", rest_of_path) == 0) {
828 dacp_player_pause (dacp_share->priv->player);
829 soup_message_set_status (message, SOUP_STATUS_NO_CONTENT);
830 } else if (g_ascii_strcasecmp ("/1/nextitem", rest_of_path) == 0) {
831 dacp_player_next_item (dacp_share->priv->player);
832 soup_message_set_status (message, SOUP_STATUS_NO_CONTENT);
833 } else if (g_ascii_strcasecmp ("/1/previtem", rest_of_path) == 0) {
834 dacp_player_prev_item (dacp_share->priv->player);
835 soup_message_set_status (message, SOUP_STATUS_NO_CONTENT);
836 } else if (g_ascii_strcasecmp ("/1/nowplayingartwork", rest_of_path)
837 == 0) {
838 guint width = 320;
839 guint height = 320;
840 guchar *artwork_filename;
841 gchar *buffer;
842 gsize buffer_len;
843
844 if (g_hash_table_lookup (query, "mw"))
845 width = atoi (g_hash_table_lookup (query, "mw"));
846 if (g_hash_table_lookup (query, "mh"))
847 height = atoi (g_hash_table_lookup (query, "mh"));
848 artwork_filename =
849 dacp_player_now_playing_artwork (dacp_share->
850 priv->player, width,
851 height);
852 if (!artwork_filename) {
853 g_debug ("No artwork for currently playing song");
854 soup_message_set_status (message,
855 SOUP_STATUS_NOT_FOUND);
856 return;
857 }
858 #ifdef HAVE_GDKPIXBUF
859 GdkPixbuf *artwork =
860 gdk_pixbuf_new_from_file_at_scale ((char *) artwork_filename,
861 width, height,
862 TRUE, NULL);
863
864 if (!artwork) {
865 g_debug ("Error loading image file");
866 g_free (artwork_filename);
867 soup_message_set_status (message,
868 SOUP_STATUS_INTERNAL_SERVER_ERROR);
869 return;
870 }
871 if (!gdk_pixbuf_save_to_buffer
872 (artwork, &buffer, &buffer_len, "png", NULL, NULL)) {
873 g_debug ("Error saving artwork to PNG");
874 g_object_unref (artwork);
875 g_free (artwork_filename);
876 soup_message_set_status (message,
877 SOUP_STATUS_INTERNAL_SERVER_ERROR);
878 return;
879 }
880 g_object_unref (artwork);
881 #else
882 if (!g_file_get_contents
883 (artwork_filename, &buffer, &buffer_len, NULL)) {
884 g_debug ("Error getting artwork data");
885 g_free (artwork_filename);
886 soup_message_set_status (message,
887 SOUP_STATUS_INTERNAL_SERVER_ERROR);
888 return;
889 }
890 #endif
891 g_free (artwork_filename);
892 soup_message_set_status (message, SOUP_STATUS_OK);
893 soup_message_set_response (message, "image/png",
894 SOUP_MEMORY_TAKE, buffer,
895 buffer_len);
896 } else if (g_ascii_strcasecmp ("/1/cue", rest_of_path) == 0) {
897 gchar *command;
898
899 command = g_hash_table_lookup (query, "command");
900
901 if (!command) {
902 g_debug ("No CUE command specified");
903 soup_message_set_status (message,
904 SOUP_STATUS_NO_CONTENT);
905 return;
906 } else if (g_ascii_strcasecmp ("clear", command) == 0) {
907 dacp_player_cue_clear (dacp_share->priv->player);
908 soup_message_set_status (message,
909 SOUP_STATUS_NO_CONTENT);
910 } else if (g_ascii_strcasecmp ("play", command) == 0) {
911 GNode *cacr;
912 gchar *record_query;
913 gchar *sort_by;
914 GHashTable *records;
915 GList *sorted_records;
916 GSList *filter_def;
917 DMAPDb *db;
918 gint index =
919 atoi (g_hash_table_lookup (query, "index"));
920
921 g_object_get (share, "db", &db, NULL);
922 record_query = g_hash_table_lookup (query, "query");
923 filter_def = _dmap_share_build_filter (record_query);
924 records = dmap_db_apply_filter (db, filter_def);
925 sorted_records = g_hash_table_get_values (records);
926 sort_by = g_hash_table_lookup (query, "sort");
927 if (g_strcmp0 (sort_by, "album") == 0) {
928 sorted_records =
929 g_list_sort_with_data (sorted_records,
930 (GCompareDataFunc)
931 daap_record_cmp_by_album,
932 db);
933 } else if (sort_by != NULL) {
934 g_warning ("Unknown sort column: %s",
935 sort_by);
936 }
937
938 dacp_player_cue_play (dacp_share->priv->player,
939 sorted_records, index);
940
941 g_list_free (sorted_records);
942 g_hash_table_unref (records);
943 dmap_share_free_filter (filter_def);
944
945 cacr = dmap_structure_add (NULL, DMAP_CC_CACR);
946 dmap_structure_add (cacr, DMAP_CC_MSTT,
947 DMAP_STATUS_OK);
948 dmap_structure_add (cacr, DMAP_CC_MIID, index);
949
950 _dmap_share_message_set_from_dmap_structure (share,
951 message,
952 cacr);
953 dmap_structure_destroy (cacr);
954 } else {
955 g_warning ("Unhandled cue command: %s", command);
956 soup_message_set_status (message,
957 SOUP_STATUS_NO_CONTENT);
958 return;
959 }
960 } else {
961 g_warning ("Unhandled ctrl-int command: %s", rest_of_path);
962 soup_message_set_status (message, SOUP_STATUS_BAD_REQUEST);
963 }
964 }
965
966 #define PAIR_TXT_LENGTH 16
967 #define PASSCODE_LENGTH 4
968
969 static gchar *
dacp_share_pairing_code(G_GNUC_UNUSED DACPShare * share,gchar * pair_txt,gchar passcode[4])970 dacp_share_pairing_code (G_GNUC_UNUSED DACPShare * share,
971 gchar * pair_txt,
972 gchar passcode[4])
973 {
974 int i;
975 GString *pairing_code;
976 gchar *pairing_string;
977 gchar *ret;
978
979 /* The pairing code is the MD5 sum of the concatenation of pair_txt
980 * with the passcode, but the passcode takes 16-bits unicodes characters */
981 pairing_string =
982 g_strnfill (PAIR_TXT_LENGTH + PASSCODE_LENGTH * 2, '\0');
983 g_strlcpy (pairing_string, pair_txt,
984 PAIR_TXT_LENGTH + PASSCODE_LENGTH * 2);
985 for (i = 0; i < 4; i++) {
986 pairing_string[PAIR_TXT_LENGTH + i * 2] = passcode[i];
987 }
988
989 pairing_code =
990 g_string_new (g_compute_checksum_for_data
991 (G_CHECKSUM_MD5, (guchar *) pairing_string,
992 PAIR_TXT_LENGTH + PASSCODE_LENGTH * 2));
993 g_string_ascii_up (pairing_code);
994 ret = pairing_code->str;
995 g_string_free (pairing_code, FALSE);
996
997 return ret;
998 }
999
1000 static void
connection_handler_cb(DMAPConnection * connection,guint status,GNode * structure,gpointer user_data)1001 connection_handler_cb (DMAPConnection * connection, guint status,
1002 GNode * structure, gpointer user_data)
1003 {
1004 gboolean connected;
1005 GHashTableIter iter;
1006 gpointer key, value;
1007 DACPShare *share = user_data;
1008 DACPRemoteInfo *remote_info = NULL;
1009 gchar *service_name = NULL;
1010 DMAPStructureItem *item = NULL;
1011 gchar *pairing_guid;
1012
1013 g_debug ("Pairing returned with code %u", status);
1014 if (SOUP_STATUS_IS_SUCCESSFUL (status)) {
1015 connected = TRUE;
1016 } else {
1017 connected = FALSE;
1018 }
1019
1020 /* Get the pairing-guid to identify this remote in the future. */
1021 if (structure)
1022 item = dmap_structure_find_item (structure, DMAP_CC_CMPG);
1023 if (item) {
1024 guint64 guid = g_value_get_int64 (&(item->content));
1025
1026 pairing_guid =
1027 g_strdup_printf ("0x%.16" G_GINT64_MODIFIER "X",
1028 guid);
1029 g_signal_emit (share, signals[ADD_GUID], 0, pairing_guid);
1030 g_free (pairing_guid);
1031 }
1032
1033 /* Find the remote that initiated this connection */
1034 g_hash_table_iter_init (&iter, share->priv->remotes);
1035 while (g_hash_table_iter_next (&iter, &key, &value)) {
1036 if (((DACPRemoteInfo *) value)->connection == connection) {
1037 service_name = (gchar *) key;
1038 remote_info = (DACPRemoteInfo *) value;
1039 break;
1040 }
1041 }
1042
1043 if (remote_info == NULL) {
1044 g_warning ("Remote for connection not found");
1045 return;
1046 }
1047
1048 /* Frees the connection */
1049 remote_info->connection = NULL;
1050 g_object_unref (connection);
1051
1052 /* FIXME: Send more detailed error info, such as wrong pair code, etc */
1053 g_signal_emit (share, signals[REMOTE_PAIRED], 0, service_name,
1054 connected);
1055 }
1056
1057 void
dacp_share_pair(DACPShare * share,gchar * service_name,gchar passcode[4])1058 dacp_share_pair (DACPShare * share, gchar * service_name, gchar passcode[4])
1059 {
1060 gchar *pairing_code;
1061 gchar *name;
1062 gchar *path;
1063 DACPRemoteInfo *remote_info;
1064
1065 remote_info = g_hash_table_lookup (share->priv->remotes,
1066 service_name);
1067
1068 if (remote_info == NULL) {
1069 g_warning ("Remote %s not found.", service_name);
1070 return;
1071 }
1072
1073 if (remote_info->connection != NULL) {
1074 g_warning ("Already pairing remote %s.", service_name);
1075 return;
1076 }
1077
1078 g_object_get (share, "name", &name, NULL);
1079
1080 remote_info->connection = DMAP_CONNECTION (dacp_connection_new (name,
1081 remote_info->host, remote_info->port,
1082 NULL,
1083 NULL));
1084 /* This is required since we don't call DMAPConnection default handler */
1085 dmap_connection_setup (remote_info->connection);
1086
1087 /* Get the remote path for pairing */
1088 pairing_code =
1089 dacp_share_pairing_code (share, remote_info->pair_txt,
1090 passcode);
1091 path = g_strdup_printf ("/pair?pairingcode=%s&servicename=%s",
1092 pairing_code, name);
1093 g_free (pairing_code);
1094
1095 g_debug ("Pairing remote in %s:%d/%s", remote_info->host,
1096 remote_info->port, path);
1097
1098 /* Let DMAPConnection do the heavy work */
1099 dmap_connection_get (remote_info->connection, path, FALSE,
1100 connection_handler_cb, share);
1101
1102 g_free (path);
1103 }
1104