1 /*
2  *  Copyright (C) 2006 Jonathan Matthew <jonathan@kaolin.hn.org>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  The Rhythmbox authors hereby grant permission for non-GPL compatible
10  *  GStreamer plugins to be used and distributed together with GStreamer
11  *  and Rhythmbox. This permission is above and beyond the permissions granted
12  *  by the GPL license by which Rhythmbox is covered. If you modify this code
13  *  you may extend this exception to your version of the code, but you are not
14  *  obligated to do so. If you do not wish to do so, delete this exception
15  *  statement from your version.
16  *
17  *  This program is distributed in the hope that it will be useful,
18  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
19  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  *  GNU General Public License for more details.
21  *
22  *  You should have received a copy of the GNU General Public License
23  *  along with this program; if not, write to the Free Software
24  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
25  *
26  */
27 
28 /*
29  * Client for out-of-process metadata reader communicating via D-BUS.
30  *
31  * How this works:
32  * - spawn rb-metadata process, with pipes
33  * - child process sets up its dbus server or whatever
34  * - if successful, child writes dbus server address to stdout; otherwise, dies.
35  * - parent opens dbus connection
36  *
37  * For each request, the parent checks if the dbus connection is still alive,
38  * and pings the child to see if it's still responding.  If the child has
39  * exited or is not responding, the parent starts a new metadata helper as
40  * described above.
41  *
42  * The child process exits after a certain period of inactivity (30s
43  * currently), so the ping message serves two purposes - it checks that the
44  * child is still capable of handling messages, and it ensures the child
45  * doesn't time out between when we check the child is still running and when
46  * we actually send it the request.
47  */
48 
49 /**
50  * SECTION:rb-metadata
51  * @short_description: metadata reader and writer interface
52  *
53  * Provides a simple synchronous interface for metadata extraction and updating.
54  */
55 
56 #include <config.h>
57 
58 #include "rb-metadata.h"
59 #include "rb-metadata-dbus.h"
60 #include "rb-debug.h"
61 #include "rb-util.h"
62 
63 #include <glib/gi18n.h>
64 #include <gio/gio.h>
65 
66 #include <unistd.h>
67 #include <sys/types.h>
68 #include <sys/signal.h>
69 #include <sys/wait.h>
70 #include <string.h>
71 #include <stdlib.h>
72 
73 static void rb_metadata_class_init (RBMetaDataClass *klass);
74 static void rb_metadata_init (RBMetaData *md);
75 static void rb_metadata_finalize (GObject *object);
76 
77 static gboolean tried_env_address = FALSE;
78 static GDBusConnection *dbus_connection = NULL;
79 static GPid metadata_child = 0;
80 static int metadata_stdout = -1;
81 static GMainContext *main_context = NULL;
82 static GMutex conn_mutex;
83 static char **saveable_types = NULL;
84 
85 struct RBMetaDataPrivate
86 {
87 	char       *media_type;
88 	char      **missing_plugins;
89 	char      **plugin_descriptions;
90 	gboolean    has_audio;
91 	gboolean    has_video;
92 	gboolean    has_other_data;
93 	GHashTable *metadata;
94 };
95 
G_DEFINE_TYPE(RBMetaData,rb_metadata,G_TYPE_OBJECT)96 G_DEFINE_TYPE (RBMetaData, rb_metadata, G_TYPE_OBJECT)
97 
98 static void
99 rb_metadata_class_init (RBMetaDataClass *klass)
100 {
101 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
102 
103 	object_class->finalize = rb_metadata_finalize;
104 
105 	g_type_class_add_private (object_class, sizeof (RBMetaDataPrivate));
106 
107 	main_context = g_main_context_new ();	/* maybe not needed? */
108 }
109 
110 static void
rb_metadata_init(RBMetaData * md)111 rb_metadata_init (RBMetaData *md)
112 {
113 	md->priv = G_TYPE_INSTANCE_GET_PRIVATE (md, RB_TYPE_METADATA, RBMetaDataPrivate);
114 }
115 
116 static void
rb_metadata_finalize(GObject * object)117 rb_metadata_finalize (GObject *object)
118 {
119 	RBMetaData *md;
120 
121 	md = RB_METADATA (object);
122 
123 	g_free (md->priv->media_type);
124 	if (md->priv->metadata)
125 		g_hash_table_destroy (md->priv->metadata);
126 
127 	G_OBJECT_CLASS (rb_metadata_parent_class)->finalize (object);
128 }
129 
130 /**
131  * rb_metadata_new:
132  *
133  * Creates a new metadata backend instance.
134  *
135  * Return value: new #RBMetaData instance
136  */
137 RBMetaData *
rb_metadata_new(void)138 rb_metadata_new (void)
139 {
140 	return RB_METADATA (g_object_new (RB_TYPE_METADATA, NULL));
141 }
142 
143 static void
kill_metadata_service(void)144 kill_metadata_service (void)
145 {
146 	if (dbus_connection) {
147 		if (g_dbus_connection_is_closed (dbus_connection) == FALSE) {
148 			rb_debug ("closing dbus connection");
149 			g_dbus_connection_close_sync (dbus_connection, NULL, NULL);
150 		} else {
151 			rb_debug ("dbus connection already closed");
152 		}
153 		g_object_unref (dbus_connection);
154 		dbus_connection = NULL;
155 	}
156 
157 	if (metadata_child) {
158 		rb_debug ("killing child process");
159 		kill (metadata_child, SIGINT);
160 		g_spawn_close_pid (metadata_child);
161 		metadata_child = 0;
162 	}
163 
164 	if (metadata_stdout != -1) {
165 		rb_debug ("closing metadata child process stdout pipe");
166 		close (metadata_stdout);
167 		metadata_stdout = -1;
168 	}
169 }
170 
171 static gboolean
ping_metadata_service(GError ** error)172 ping_metadata_service (GError **error)
173 {
174 	GDBusMessage *message;
175 	GDBusMessage *response;
176 
177 	if (g_dbus_connection_is_closed (dbus_connection))
178 		return FALSE;
179 
180 	message = g_dbus_message_new_method_call (RB_METADATA_DBUS_NAME,
181 						  RB_METADATA_DBUS_OBJECT_PATH,
182 						  RB_METADATA_DBUS_INTERFACE,
183 						  "ping");
184 	response = g_dbus_connection_send_message_with_reply_sync (dbus_connection,
185 								   message,
186 								   G_DBUS_MESSAGE_FLAGS_NONE,
187 								   RB_METADATA_DBUS_TIMEOUT,
188 								   NULL,
189 								   NULL,
190 								   error);
191 	g_object_unref (message);
192 
193 	if (*error != NULL) {
194 		/* ignore 'no reply', just means the service is dead */
195 		if ((*error)->domain == G_DBUS_ERROR && (*error)->code == G_DBUS_ERROR_NO_REPLY) {
196 			g_clear_error (error);
197 		}
198 		return FALSE;
199 	}
200 	g_object_unref (response);
201 	return TRUE;
202 }
203 
204 static gboolean
start_metadata_service(GError ** error)205 start_metadata_service (GError **error)
206 {
207 	GIOChannel *stdout_channel;
208 	GIOStatus status;
209 	gchar *dbus_address = NULL;
210 	char *saveable_type_list;
211 	GVariant *response_body;
212 
213 	if (dbus_connection) {
214 		if (ping_metadata_service (error))
215 			return TRUE;
216 
217 		/* Metadata service is broken.  Kill it, and if we haven't run
218 		 * into any errors yet, we can try to restart it.
219 		 */
220 		kill_metadata_service ();
221 
222 		if (*error)
223 			return FALSE;
224 	}
225 
226 	if (!tried_env_address) {
227 		const char *addr = g_getenv ("RB_DBUS_METADATA_ADDRESS");
228 		tried_env_address = TRUE;
229 		if (addr) {
230 			rb_debug ("trying metadata service address %s (from environment)", addr);
231 			dbus_address = g_strdup (addr);
232 			metadata_child = 0;
233 		}
234 	}
235 
236 	if (dbus_address == NULL) {
237 		GPtrArray *argv;
238 		gboolean res;
239 		char **debug_args;
240 		GError *local_error;
241 		int i;
242 
243 		argv = g_ptr_array_new ();
244 		/*
245 		 * Normally, we find the metadata helper in the libexec dir,
246 		 * but when --enable-uninstalled-build is specified, we look
247 		 * in the directory it's built in.
248 		 */
249 #ifdef USE_UNINSTALLED_DIRS
250 		g_ptr_array_add (argv, METADATA_UNINSTALLED_DIR "/rhythmbox-metadata");
251 #else
252 		g_ptr_array_add (argv, LIBEXEC_DIR G_DIR_SEPARATOR_S INSTALLED_METADATA_HELPER);
253 #endif
254 		debug_args = rb_debug_get_args ();
255 		i = 0;
256 		while (debug_args[i] != NULL) {
257 			g_ptr_array_add (argv, debug_args[i]);
258 			i++;
259 		}
260 
261 		g_ptr_array_add (argv, "unix:tmpdir=/tmp");
262 		g_ptr_array_add (argv, NULL);
263 
264 		local_error = NULL;
265 		res = g_spawn_async_with_pipes (NULL,
266 						(char **)argv->pdata,
267 						NULL,
268 						0,
269 						NULL, NULL,
270 						&metadata_child,
271 						NULL,
272 						&metadata_stdout,
273 						NULL,
274 						&local_error);
275 		g_ptr_array_free (argv, TRUE);
276 		g_strfreev (debug_args);
277 
278 		if (res == FALSE) {
279 			g_propagate_error (error, local_error);
280 			return FALSE;
281 		}
282 
283 		stdout_channel = g_io_channel_unix_new (metadata_stdout);
284 		status = g_io_channel_read_line (stdout_channel, &dbus_address, NULL, NULL, error);
285 		g_io_channel_unref (stdout_channel);
286 		if (status != G_IO_STATUS_NORMAL) {
287 			kill_metadata_service ();
288 			return FALSE;
289 		}
290 
291 		g_strchomp (dbus_address);
292 		rb_debug ("Got metadata helper D-BUS address %s", dbus_address);
293 	}
294 
295 	dbus_connection = g_dbus_connection_new_for_address_sync (dbus_address,
296 								  G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
297 								  NULL,
298 								  NULL,
299 								  error);
300 	g_free (dbus_address);
301 	if (*error != NULL) {
302 		kill_metadata_service ();
303 		return FALSE;
304 	}
305 
306 	g_dbus_connection_set_exit_on_close (dbus_connection, FALSE);
307 
308 	rb_debug ("Metadata process %d started", metadata_child);
309 
310 	/* now ask it what types it can re-tag */
311 	if (saveable_types != NULL) {
312 		g_strfreev (saveable_types);
313 	}
314 
315 	response_body = g_dbus_connection_call_sync (dbus_connection,
316 						     RB_METADATA_DBUS_NAME,
317 						     RB_METADATA_DBUS_OBJECT_PATH,
318 						     RB_METADATA_DBUS_INTERFACE,
319 						     "getSaveableTypes",
320 						     NULL,
321 						     NULL,
322 						     G_DBUS_CALL_FLAGS_NONE,
323 						     RB_METADATA_DBUS_TIMEOUT,
324 						     NULL,
325 						     error);
326 	if (response_body == NULL) {
327 		rb_debug ("saveable type query failed: %s", (*error)->message);
328 		return FALSE;
329 	}
330 
331 	g_variant_get (response_body, "(^as)", &saveable_types);
332 	if (saveable_types != NULL) {
333 		saveable_type_list = g_strjoinv (", ", saveable_types);
334 		rb_debug ("saveable types from metadata helper: %s", saveable_type_list);
335 		g_free (saveable_type_list);
336 	} else {
337 		rb_debug ("unable to save metadata for any file types");
338 	}
339 	g_variant_unref (response_body);
340 
341 	return TRUE;
342 }
343 
344 /**
345  * rb_metadata_reset:
346  * @md: a #RBMetaData
347  *
348  * Resets the state of the metadata interface.  Call this before
349  * setting tags to be written to a file.
350  */
351 void
rb_metadata_reset(RBMetaData * md)352 rb_metadata_reset (RBMetaData *md)
353 {
354 	g_free (md->priv->media_type);
355 	md->priv->media_type = NULL;
356 
357 	if (md->priv->metadata)
358 		g_hash_table_destroy (md->priv->metadata);
359 	md->priv->metadata = g_hash_table_new_full (g_direct_hash,
360 						    g_direct_equal,
361 						    NULL,
362 						    (GDestroyNotify)rb_value_free);
363 }
364 
365 /**
366  * rb_metadata_load:
367  * @md: a #RBMetaData
368  * @uri: URI from which to load metadata
369  * @error: returns error information
370  *
371  * Reads metadata information from the specified URI.
372  * Once this has returned successfully (with *error == NULL),
373  * rb_metadata_get, rb_metadata_get_media_type, rb_metadata_has_missing_plugins,
374  * and rb_metadata_get_missing_plugins can usefully be called.
375  */
376 void
rb_metadata_load(RBMetaData * md,const char * uri,GError ** error)377 rb_metadata_load (RBMetaData *md,
378 		  const char *uri,
379 		  GError **error)
380 {
381 	GVariant *response;
382 	GError *fake_error = NULL;
383 
384 	if (error == NULL)
385 		error = &fake_error;
386 
387 	rb_metadata_reset (md);
388 	if (uri == NULL)
389 		return;
390 	g_mutex_lock (&conn_mutex);
391 
392 	start_metadata_service (error);
393 
394 	if (*error == NULL) {
395 		rb_debug ("sending metadata load request: %s", uri);
396 		response = g_dbus_connection_call_sync (dbus_connection,
397 							RB_METADATA_DBUS_NAME,
398 							RB_METADATA_DBUS_OBJECT_PATH,
399 							RB_METADATA_DBUS_INTERFACE,
400 							"load",
401 							g_variant_new ("(s)", uri),
402 							NULL, /* complicated return type */
403 							G_DBUS_CALL_FLAGS_NONE,
404 							RB_METADATA_DBUS_TIMEOUT,
405 							NULL,
406 							error);
407 	}
408 
409 	if (*error == NULL) {
410 		GVariantIter *metadata;
411 		gboolean ok = FALSE;
412 		int error_code;
413 		char *error_string = NULL;
414 
415 		g_variant_get (response,
416 			       "(^as^asbbbsbisa{iv})",
417 			       &md->priv->missing_plugins,
418 			       &md->priv->plugin_descriptions,
419 			       &md->priv->has_audio,
420 			       &md->priv->has_video,
421 			       &md->priv->has_other_data,
422 			       &md->priv->media_type,
423 			       &ok,
424 			       &error_code,
425 			       &error_string,
426 			       &metadata);
427 
428 		if (ok) {
429 			guint32 key;
430 			GVariant *value;
431 
432 			while (g_variant_iter_next (metadata, "{iv}", &key, &value)) {
433 				GValue *val = g_slice_new0 (GValue);
434 
435 				switch (rb_metadata_get_field_type (key)) {
436 				case G_TYPE_STRING:
437 					g_value_init (val, G_TYPE_STRING);
438 					g_value_set_string (val, g_variant_get_string (value, NULL));
439 					break;
440 				case G_TYPE_ULONG:
441 					g_value_init (val, G_TYPE_ULONG);
442 					g_value_set_ulong (val, g_variant_get_uint32 (value));
443 					break;
444 				case G_TYPE_DOUBLE:
445 					g_value_init (val, G_TYPE_DOUBLE);
446 					g_value_set_double (val, g_variant_get_double (value));
447 					break;
448 				default:
449 					g_assert_not_reached ();
450 					break;
451 				}
452 				g_hash_table_insert (md->priv->metadata, GINT_TO_POINTER (key), val);
453 				g_variant_unref (value);
454 			}
455 
456 		} else {
457 			g_set_error (error, RB_METADATA_ERROR,
458 				     error_code,
459 				     "%s", error_string);
460 		}
461 		g_variant_iter_free (metadata);
462 
463 		/* if we're missing some plugins, we'll need to make sure the
464 		 * metadata helper rereads the registry before the next load.
465 		 * the easiest way to do this is to kill it.
466 		 */
467 		if (*error == NULL && g_strv_length (md->priv->missing_plugins) > 0) {
468 			rb_debug ("missing plugins; killing metadata service to force registry reload");
469 			kill_metadata_service ();
470 		}
471 	}
472 	if (fake_error)
473 		g_error_free (fake_error);
474 
475 	g_mutex_unlock (&conn_mutex);
476 }
477 
478 /**
479  * rb_metadata_get_media_type:
480  * @md: a #RBMetaData
481  *
482  * Returns the type of the file from which metadata was read.
483  * This may look like a MIME type, but it isn't.
484  *
485  * Return value: media type string
486  */
487 const char *
rb_metadata_get_media_type(RBMetaData * md)488 rb_metadata_get_media_type (RBMetaData *md)
489 {
490 	return md->priv->media_type;
491 }
492 
493 /**
494  * rb_metadata_has_missing_plugins:
495  * @md: a #RBMetaData
496  *
497  * If the metadata reader could not decode the file it was asked to
498  * because one or more media framework plugins (specifically, for the
499  * existing implementations, GStreamer plugins) required are missing,
500  * this will return TRUE.
501  *
502  * Return value: TRUE if required plugins are missing
503  */
504 gboolean
rb_metadata_has_missing_plugins(RBMetaData * md)505 rb_metadata_has_missing_plugins (RBMetaData *md)
506 {
507 	return (md->priv->missing_plugins != NULL &&
508 	        g_strv_length (md->priv->missing_plugins) > 0);
509 }
510 
511 /**
512  * rb_metadata_get_missing_plugins:
513  * @md: a #RBMetaData
514  * @missing_plugins: (out) (array zero-terminated=1): returns machine-readable
515  * missing plugin information
516  * @plugin_descriptions: (out) (array zero-terminated=1): returns human-readable
517  * missing plugin descriptions
518  *
519  * This function returns the information used to request automatic
520  * installation of media framework plugins required to decode the target URI.
521  * Use g_strfreev() to free the returned information arrays.
522  *
523  * Return value: TRUE if missing plugin information was returned
524  */
525 gboolean
rb_metadata_get_missing_plugins(RBMetaData * md,char *** missing_plugins,char *** plugin_descriptions)526 rb_metadata_get_missing_plugins (RBMetaData *md,
527 				 char ***missing_plugins,
528 				 char ***plugin_descriptions)
529 {
530 	if (rb_metadata_has_missing_plugins (md) == FALSE) {
531 		return FALSE;
532 	}
533 
534 	*missing_plugins = g_strdupv (md->priv->missing_plugins);
535 	*plugin_descriptions = g_strdupv (md->priv->plugin_descriptions);
536 	return TRUE;
537 }
538 
539 
540 /**
541  * rb_metadata_get:
542  * @md: a #RBMetaData
543  * @field: the #RBMetaDataField to retrieve
544  * @val: (out caller-allocates) (transfer full): returns the field value
545  *
546  * Retrieves the value of a metadata field extracted from the target URI.
547  * If the target URI contained no value for the field, returns FALSE.
548  *
549  * Return value: TRUE if a value was returned
550  */
551 gboolean
rb_metadata_get(RBMetaData * md,RBMetaDataField field,GValue * ret)552 rb_metadata_get (RBMetaData *md, RBMetaDataField field, GValue *ret)
553 {
554 	GValue *val;
555 	if (!md->priv->metadata)
556 		return FALSE;
557 
558 	if ((val = g_hash_table_lookup (md->priv->metadata,
559 					GINT_TO_POINTER (field)))) {
560 		g_value_init (ret, G_VALUE_TYPE (val));
561 		g_value_copy (val, ret);
562 		return TRUE;
563 	}
564 	return FALSE;
565 }
566 
567 /**
568  * rb_metadata_set:
569  * @md: a #RBMetaData
570  * @field: the #RBMetaDataField to set
571  * @val: the value to set
572  *
573  * Sets a metadata field value.  The value is only stored inside the
574  * #RBMetaData object until rb_metadata_save is called.
575  *
576  * Return value: TRUE if the field is valid
577  */
578 gboolean
rb_metadata_set(RBMetaData * md,RBMetaDataField field,const GValue * val)579 rb_metadata_set (RBMetaData *md, RBMetaDataField field,
580 		 const GValue *val)
581 {
582 	GValue *newval;
583 	GType type;
584 
585 	type = rb_metadata_get_field_type (field);
586 	g_return_val_if_fail (type == G_VALUE_TYPE (val), FALSE);
587 
588 	newval = g_slice_new0 (GValue);
589 	g_value_init (newval, type);
590 	g_value_copy (val, newval);
591 
592 	g_hash_table_insert (md->priv->metadata, GINT_TO_POINTER (field),
593 			     newval);
594 	return TRUE;
595 }
596 
597 /**
598  * rb_metadata_can_save:
599  * @md: a #RBMetaData
600  * @media_type: the media type string to check
601  *
602  * Checks if the metadata writer is capable of updating file metadata
603  * for a given media type.
604  *
605  * Return value: TRUE if the file metadata for the given media type can be updated
606  */
607 gboolean
rb_metadata_can_save(RBMetaData * md,const char * media_type)608 rb_metadata_can_save (RBMetaData *md, const char *media_type)
609 {
610 	GError *error = NULL;
611 	gboolean result = FALSE;
612 	int i = 0;
613 
614 	g_mutex_lock (&conn_mutex);
615 
616 	if (saveable_types == NULL) {
617 		if (start_metadata_service (&error) == FALSE) {
618 			g_warning ("unable to start metadata service: %s", error->message);
619 			g_mutex_unlock (&conn_mutex);
620 			g_error_free (error);
621 			return FALSE;
622 		}
623 	}
624 
625 	if (saveable_types != NULL) {
626 		for (i = 0; saveable_types[i] != NULL; i++) {
627 			if (g_str_equal (media_type, saveable_types[i])) {
628 				result = TRUE;
629 				break;
630 			}
631 		}
632 	}
633 
634 	g_mutex_unlock (&conn_mutex);
635 	return result;
636 }
637 
638 /**
639  * rb_metadata_get_saveable_types:
640  * @md: a #RBMetaData
641  *
642  * Constructs a list of the media types for which the metadata backend
643  * implements tag saving.
644  *
645  * Return value: (transfer full) (array zero-terminated=1): a NULL-terminated
646  * array of media type strings.  Use g_strfreev to free it.
647  */
648 char **
rb_metadata_get_saveable_types(RBMetaData * md)649 rb_metadata_get_saveable_types (RBMetaData *md)
650 {
651 	return g_strdupv (saveable_types);
652 }
653 
654 /**
655  * rb_metadata_save:
656  * @md: a #RBMetaData
657  * @uri: the target URI
658  * @error: returns error information
659  *
660  * Saves all metadata changes made with rb_metadata_set to the
661  * target URI.
662  */
663 void
rb_metadata_save(RBMetaData * md,const char * uri,GError ** error)664 rb_metadata_save (RBMetaData *md, const char *uri, GError **error)
665 {
666 	GVariant *response;
667 	GError *fake_error = NULL;
668 
669 	if (error == NULL)
670 		error = &fake_error;
671 
672 	g_mutex_lock (&conn_mutex);
673 
674 	start_metadata_service (error);
675 
676 	if (*error == NULL) {
677 		response = g_dbus_connection_call_sync (dbus_connection,
678 							RB_METADATA_DBUS_NAME,
679 							RB_METADATA_DBUS_OBJECT_PATH,
680 							RB_METADATA_DBUS_INTERFACE,
681 							"save",
682 							g_variant_new ("(sa{iv})",
683 								       uri,
684 								       rb_metadata_dbus_get_variant_builder (md)),
685 							NULL,
686 							G_DBUS_CALL_FLAGS_NONE,
687 							RB_METADATA_SAVE_DBUS_TIMEOUT,
688 							NULL,
689 							error);
690 	}
691 
692 	if (*error == NULL) {
693 		gboolean ok = TRUE;
694 		int error_code;
695 		const char *error_message;
696 
697 		g_variant_get (response, "(bis)", &ok, &error_code, &error_message);
698 		if (ok == FALSE) {
699 			g_set_error (error, RB_METADATA_ERROR,
700 				     error_code,
701 				     "%s", error_message);
702 		}
703 
704 		g_variant_unref (response);
705 	}
706 
707 	if (fake_error)
708 		g_error_free (fake_error);
709 
710 	g_mutex_unlock (&conn_mutex);
711 }
712 
713 gboolean
rb_metadata_has_audio(RBMetaData * md)714 rb_metadata_has_audio (RBMetaData *md)
715 {
716 	return md->priv->has_audio;
717 }
718 
719 gboolean
rb_metadata_has_video(RBMetaData * md)720 rb_metadata_has_video (RBMetaData *md)
721 {
722 	return md->priv->has_video;
723 }
724 
725 gboolean
rb_metadata_has_other_data(RBMetaData * md)726 rb_metadata_has_other_data (RBMetaData *md)
727 {
728 	return md->priv->has_other_data;
729 }
730