2006-10-30 James Livingston <doclivingston@gmail.com>
[rhythmbox.git] / rhythmdb / rhythmdb.c
blob1b8f1c9d5848a63b2457899ba81307f4db67ae03
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
3 * arch-tag: Implementation of RhythmDB - Rhythmbox backend queryable database
5 * Copyright (C) 2003,2004 Colin Walters <walters@gnome.org>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
23 #include "config.h"
25 #define G_IMPLEMENT_INLINES 1
26 #define __RHYTHMDB_C__
27 #include "rhythmdb.h"
28 #undef G_IMPLEMENT_INLINES
30 #include <string.h>
31 #include <libxml/tree.h>
32 #include <glib.h>
33 #include <glib-object.h>
34 #include <glib/gi18n.h>
35 #include <gobject/gvaluecollector.h>
36 #include <gdk/gdk.h>
37 #include <gconf/gconf-client.h>
39 #include "rb-marshal.h"
40 #include "rb-file-helpers.h"
41 #include "rb-debug.h"
42 #include "rb-util.h"
43 #include "rb-cut-and-paste-code.h"
44 #include "rb-preferences.h"
45 #include "eel-gconf-extensions.h"
46 #include "rhythmdb-private.h"
47 #include "rhythmdb-property-model.h"
48 #include "rb-dialog.h"
51 #define RB_PARSE_NICK_START (xmlChar *) "["
52 #define RB_PARSE_NICK_END (xmlChar *) "]"
54 GType rhythmdb_property_type_map[RHYTHMDB_NUM_PROPERTIES];
56 #define RHYTHMDB_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RHYTHMDB_TYPE, RhythmDBPrivate))
57 G_DEFINE_ABSTRACT_TYPE(RhythmDB, rhythmdb, G_TYPE_OBJECT)
59 typedef struct
61 RhythmDB *db;
62 GPtrArray *query;
63 guint propid;
64 RhythmDBQueryResults *results;
65 gboolean cancel;
66 } RhythmDBQueryThreadData;
68 typedef struct
70 RhythmDB *db;
71 char *uri;
72 RhythmDBEntryType type;
73 } RhythmDBAddThreadData;
75 typedef struct
77 enum {
78 RHYTHMDB_ACTION_STAT,
79 RHYTHMDB_ACTION_LOAD,
80 RHYTHMDB_ACTION_SYNC
81 } type;
82 RBRefString *uri;
83 RhythmDBEntryType entry_type;
84 } RhythmDBAction;
86 static void rhythmdb_finalize (GObject *object);
87 static void rhythmdb_set_property (GObject *object,
88 guint prop_id,
89 const GValue *value,
90 GParamSpec *pspec);
91 static void rhythmdb_get_property (GObject *object,
92 guint prop_id,
93 GValue *value,
94 GParamSpec *pspec);
95 static void rhythmdb_thread_create (RhythmDB *db,
96 GThreadPool *pool,
97 GThreadFunc func,
98 gpointer data);
99 static void rhythmdb_read_enter (RhythmDB *db);
100 static void rhythmdb_read_leave (RhythmDB *db);
101 static gboolean rhythmdb_idle_poll_events (RhythmDB *db);
102 static gpointer add_thread_main (RhythmDBAddThreadData *data);
103 static gpointer action_thread_main (RhythmDB *db);
104 static gpointer query_thread_main (RhythmDBQueryThreadData *data);
105 static void rhythmdb_entry_set_mount_point (RhythmDB *db,
106 RhythmDBEntry *entry,
107 const gchar *realuri);
109 static gboolean free_entry_changes (RhythmDBEntry *entry,
110 GSList *changes,
111 RhythmDB *db);
112 static gboolean rhythmdb_idle_save (RhythmDB *db);
113 static void library_location_changed_cb (GConfClient *client,
114 guint cnxn_id,
115 GConfEntry *entry,
116 RhythmDB *db);
117 static void rhythmdb_sync_library_location (RhythmDB *db);
118 static void rhythmdb_entry_sync_mirrored (RhythmDBEntry *entry,
119 guint propid);
120 static void rhythmdb_register_core_entry_types (RhythmDB *db);
121 static gboolean rhythmdb_entry_extra_metadata_accumulator (GSignalInvocationHint *ihint,
122 GValue *return_accu,
123 const GValue *handler_return,
124 gpointer data);
126 enum
128 PROP_0,
129 PROP_NAME,
130 PROP_DRY_RUN,
131 PROP_NO_UPDATE,
134 enum
136 ENTRY_ADDED,
137 ENTRY_CHANGED,
138 ENTRY_DELETED,
139 ENTRY_EXTRA_METADATA_REQUEST,
140 ENTRY_EXTRA_METADATA_NOTIFY,
141 ENTRY_EXTRA_METADATA_GATHER,
142 LOAD_COMPLETE,
143 SAVE_COMPLETE,
144 SAVE_ERROR,
145 READ_ONLY,
146 LAST_SIGNAL
149 static guint rhythmdb_signals[LAST_SIGNAL] = { 0 };
151 static void
152 rhythmdb_class_init (RhythmDBClass *klass)
154 GObjectClass *object_class = G_OBJECT_CLASS (klass);
156 object_class->finalize = rhythmdb_finalize;
158 object_class->set_property = rhythmdb_set_property;
159 object_class->get_property = rhythmdb_get_property;
161 g_object_class_install_property (object_class,
162 PROP_NAME,
163 g_param_spec_string ("name",
164 "name",
165 "name",
166 NULL,
167 G_PARAM_READWRITE));
169 g_object_class_install_property (object_class,
170 PROP_DRY_RUN,
171 g_param_spec_boolean ("dry-run",
172 "dry run",
173 "Whether or not changes should be saved",
174 FALSE,
175 G_PARAM_READWRITE));
176 g_object_class_install_property (object_class,
177 PROP_NO_UPDATE,
178 g_param_spec_boolean ("no-update",
179 "no update",
180 "Whether or not to update the database",
181 FALSE,
182 G_PARAM_READWRITE));
183 rhythmdb_signals[ENTRY_ADDED] =
184 g_signal_new ("entry_added",
185 RHYTHMDB_TYPE,
186 G_SIGNAL_RUN_LAST,
187 G_STRUCT_OFFSET (RhythmDBClass, entry_added),
188 NULL, NULL,
189 g_cclosure_marshal_VOID__BOXED,
190 G_TYPE_NONE,
191 1, RHYTHMDB_TYPE_ENTRY);
193 rhythmdb_signals[ENTRY_DELETED] =
194 g_signal_new ("entry_deleted",
195 RHYTHMDB_TYPE,
196 G_SIGNAL_RUN_LAST,
197 G_STRUCT_OFFSET (RhythmDBClass, entry_deleted),
198 NULL, NULL,
199 g_cclosure_marshal_VOID__BOXED,
200 G_TYPE_NONE,
201 1, RHYTHMDB_TYPE_ENTRY);
203 rhythmdb_signals[ENTRY_CHANGED] =
204 g_signal_new ("entry_changed",
205 RHYTHMDB_TYPE,
206 G_SIGNAL_RUN_LAST,
207 G_STRUCT_OFFSET (RhythmDBClass, entry_changed),
208 NULL, NULL,
209 rb_marshal_VOID__BOXED_POINTER,
210 G_TYPE_NONE, 2,
211 RHYTHMDB_TYPE_ENTRY, G_TYPE_POINTER);
213 rhythmdb_signals[ENTRY_EXTRA_METADATA_REQUEST] =
214 g_signal_new ("entry_extra_metadata_request",
215 G_OBJECT_CLASS_TYPE (object_class),
216 G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
217 G_STRUCT_OFFSET (RhythmDBClass, entry_extra_metadata_request),
218 rhythmdb_entry_extra_metadata_accumulator, NULL,
219 rb_marshal_BOXED__BOXED,
220 G_TYPE_VALUE, 1,
221 RHYTHMDB_TYPE_ENTRY);
223 rhythmdb_signals[ENTRY_EXTRA_METADATA_NOTIFY] =
224 g_signal_new ("entry_extra_metadata_notify",
225 G_OBJECT_CLASS_TYPE (object_class),
226 G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
227 G_STRUCT_OFFSET (RhythmDBClass, entry_extra_metadata_notify),
228 NULL, NULL,
229 rb_marshal_VOID__BOXED_STRING_BOXED,
230 G_TYPE_NONE, 3,
231 RHYTHMDB_TYPE_ENTRY, G_TYPE_STRING, G_TYPE_VALUE);
233 rhythmdb_signals[ENTRY_EXTRA_METADATA_GATHER] =
234 g_signal_new ("entry_extra_metadata_gather",
235 G_OBJECT_CLASS_TYPE (object_class),
236 G_SIGNAL_RUN_LAST,
237 G_STRUCT_OFFSET (RhythmDBClass, entry_extra_metadata_gather),
238 NULL, NULL,
239 #if (GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 10)
240 rb_marshal_VOID__BOXED_BOXED,
241 G_TYPE_NONE, 2,
242 RHYTHMDB_TYPE_ENTRY, G_TYPE_HASH_TABLE);
243 #else
244 /* work with glib < 2.10 */
245 rb_marshal_VOID__BOXED_POINTER,
246 G_TYPE_NONE, 2,
247 RHYTHMDB_TYPE_ENTRY, G_TYPE_POINTER);
248 #endif
250 rhythmdb_signals[LOAD_COMPLETE] =
251 g_signal_new ("load_complete",
252 RHYTHMDB_TYPE,
253 G_SIGNAL_RUN_LAST,
254 G_STRUCT_OFFSET (RhythmDBClass, load_complete),
255 NULL, NULL,
256 g_cclosure_marshal_VOID__VOID,
257 G_TYPE_NONE,
260 rhythmdb_signals[SAVE_COMPLETE] =
261 g_signal_new ("save_complete",
262 RHYTHMDB_TYPE,
263 G_SIGNAL_RUN_LAST,
264 G_STRUCT_OFFSET (RhythmDBClass, save_complete),
265 NULL, NULL,
266 g_cclosure_marshal_VOID__VOID,
267 G_TYPE_NONE,
270 rhythmdb_signals[SAVE_ERROR] =
271 g_signal_new ("save-error",
272 G_OBJECT_CLASS_TYPE (object_class),
273 G_SIGNAL_RUN_LAST,
274 G_STRUCT_OFFSET (RhythmDBClass, save_error),
275 NULL, NULL,
276 rb_marshal_VOID__STRING_POINTER,
277 G_TYPE_NONE,
279 G_TYPE_STRING,
280 G_TYPE_POINTER);
282 rhythmdb_signals[READ_ONLY] =
283 g_signal_new ("read-only",
284 G_OBJECT_CLASS_TYPE (object_class),
285 G_SIGNAL_RUN_LAST,
286 G_STRUCT_OFFSET (RhythmDBClass, read_only),
287 NULL, NULL,
288 g_cclosure_marshal_VOID__BOOLEAN,
289 G_TYPE_NONE,
291 G_TYPE_BOOLEAN);
293 g_type_class_add_private (klass, sizeof (RhythmDBPrivate));
296 static gboolean
297 metadata_field_from_prop (RhythmDBPropType prop,
298 RBMetaDataField *field)
300 switch (prop) {
301 case RHYTHMDB_PROP_TITLE:
302 *field = RB_METADATA_FIELD_TITLE;
303 return TRUE;
304 case RHYTHMDB_PROP_ARTIST:
305 *field = RB_METADATA_FIELD_ARTIST;
306 return TRUE;
307 case RHYTHMDB_PROP_ALBUM:
308 *field = RB_METADATA_FIELD_ALBUM;
309 return TRUE;
310 case RHYTHMDB_PROP_GENRE:
311 *field = RB_METADATA_FIELD_GENRE;
312 return TRUE;
313 case RHYTHMDB_PROP_TRACK_NUMBER:
314 *field = RB_METADATA_FIELD_TRACK_NUMBER;
315 return TRUE;
316 case RHYTHMDB_PROP_DISC_NUMBER:
317 *field = RB_METADATA_FIELD_DISC_NUMBER;
318 return TRUE;
319 case RHYTHMDB_PROP_DATE:
320 *field = RB_METADATA_FIELD_DATE;
321 return TRUE;
322 case RHYTHMDB_PROP_TRACK_GAIN:
323 *field = RB_METADATA_FIELD_TRACK_GAIN;
324 return TRUE;
325 case RHYTHMDB_PROP_TRACK_PEAK:
326 *field = RB_METADATA_FIELD_TRACK_PEAK;
327 return TRUE;
328 case RHYTHMDB_PROP_ALBUM_GAIN:
329 *field = RB_METADATA_FIELD_ALBUM_GAIN;
330 return TRUE;
331 case RHYTHMDB_PROP_ALBUM_PEAK:
332 *field = RB_METADATA_FIELD_ALBUM_PEAK;
333 return TRUE;
334 case RHYTHMDB_PROP_MUSICBRAINZ_TRACKID:
335 *field = RB_METADATA_FIELD_MUSICBRAINZ_TRACKID;
336 return TRUE;
337 default:
338 return FALSE;
342 static GType
343 extract_gtype_from_enum_entry (RhythmDB *db,
344 GEnumClass *klass,
345 guint i)
347 GType ret;
348 GEnumValue *value;
349 RBMetaDataField field;
350 char *typename;
351 char *typename_end;
353 value = g_enum_get_value (klass, i);
355 typename = strstr (value->value_nick, "(");
356 g_assert (typename != NULL);
358 typename_end = strstr (typename, ")");
359 g_assert (typename_end);
361 typename++;
362 typename = g_strndup (typename, typename_end-typename);
363 ret = g_type_from_name (typename);
364 g_free (typename);
366 /* Check to see whether this is a property that maps to
367 a RBMetaData property. */
368 if (metadata_field_from_prop (value->value, &field))
369 g_assert (ret == rb_metadata_get_field_type (field));
370 return ret;
373 static xmlChar *
374 extract_nice_name_from_enum_entry (RhythmDB *db,
375 GEnumClass *klass,
376 guint i)
378 GEnumValue *value;
379 xmlChar *nick;
380 const xmlChar *name;
381 const xmlChar *name_end;
383 value = g_enum_get_value (klass, i);
384 nick = BAD_CAST value->value_nick;
386 name = xmlStrstr (nick, RB_PARSE_NICK_START);
387 g_return_val_if_fail (name != NULL, NULL);
388 name_end = xmlStrstr (name, RB_PARSE_NICK_END);
389 name++;
391 return xmlStrndup (name, name_end - name);
394 static void
395 rhythmdb_init (RhythmDB *db)
397 guint i;
398 GEnumClass *prop_class;
400 db->priv = RHYTHMDB_GET_PRIVATE (db);
402 db->priv->action_queue = g_async_queue_new ();
403 db->priv->event_queue = g_async_queue_new ();
404 db->priv->restored_queue = g_async_queue_new ();
406 db->priv->query_thread_pool = g_thread_pool_new ((GFunc)query_thread_main,
407 NULL,
408 -1, FALSE, NULL);
409 /* Limit this pool to 3 threads. They'll each be thrashing the disk,
410 * so parallelism is limited.
412 db->priv->add_thread_pool = g_thread_pool_new ((GFunc)add_thread_main,
413 NULL,
414 3, FALSE, NULL);
416 db->priv->metadata = rb_metadata_new ();
418 prop_class = g_type_class_ref (RHYTHMDB_TYPE_PROP_TYPE);
420 g_assert (prop_class->n_values == RHYTHMDB_NUM_PROPERTIES);
421 db->priv->column_xml_names = g_new0 (xmlChar *, RHYTHMDB_NUM_PROPERTIES);
423 /* Now, extract the GType and XML tag of each column from the
424 * enum descriptions, and cache that for later use. */
425 for (i = 0; i < prop_class->n_values; i++) {
426 rhythmdb_property_type_map[i] = extract_gtype_from_enum_entry (db, prop_class, i);
427 g_assert (rhythmdb_property_type_map[i] != G_TYPE_INVALID);
429 db->priv->column_xml_names[i] = extract_nice_name_from_enum_entry (db, prop_class, i);
430 g_assert (db->priv->column_xml_names[i]);
433 g_type_class_unref (prop_class);
435 db->priv->propname_map = g_hash_table_new (g_str_hash, g_str_equal);
437 for (i = 0; i < RHYTHMDB_NUM_PROPERTIES; i++) {
438 const xmlChar *name = rhythmdb_nice_elt_name_from_propid (db, i);
439 g_hash_table_insert (db->priv->propname_map, (gpointer) name, GINT_TO_POINTER (i));
442 db->priv->entry_type_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
443 db->priv->entry_type_map_mutex = g_mutex_new ();
444 db->priv->entry_type_mutex = g_mutex_new ();
445 rhythmdb_register_core_entry_types (db);
447 db->priv->stat_events = g_hash_table_new_full (gnome_vfs_uri_hash,
448 (GEqualFunc) gnome_vfs_uri_equal,
449 (GDestroyNotify) gnome_vfs_uri_unref,
450 NULL);
451 db->priv->stat_mutex = g_mutex_new ();
453 db->priv->change_mutex = g_mutex_new ();
455 db->priv->changed_entries = g_hash_table_new_full (NULL,
456 NULL,
457 (GDestroyNotify) rhythmdb_entry_unref,
458 NULL);
459 db->priv->added_entries = g_hash_table_new_full (NULL,
460 NULL,
461 (GDestroyNotify) rhythmdb_entry_unref,
462 NULL);
463 db->priv->deleted_entries = g_hash_table_new_full (NULL,
464 NULL,
465 (GDestroyNotify) rhythmdb_entry_unref,
466 NULL);
468 db->priv->event_poll_id = g_idle_add ((GSourceFunc) rhythmdb_idle_poll_events, db);
470 db->priv->saving_condition = g_cond_new ();
471 db->priv->saving_mutex = g_mutex_new ();
473 db->priv->can_save = TRUE;
474 db->priv->exiting = FALSE;
475 db->priv->saving = FALSE;
476 db->priv->dirty = FALSE;
478 db->priv->empty_string = rb_refstring_new ("");
479 db->priv->octet_stream_str = rb_refstring_new ("application/octet-stream");
481 db->priv->next_entry_id = 1;
483 rhythmdb_init_monitoring (db);
486 static GError *
487 make_access_failed_error (const char *uri, GnomeVFSResult result)
489 char *unescaped;
490 char *utf8ised;
491 GError *error;
493 /* make sure the URI we put in the error message is valid utf8 */
494 unescaped = gnome_vfs_unescape_string_for_display (uri);
495 utf8ised = rb_make_valid_utf8 (unescaped, '?');
497 error = g_error_new (RHYTHMDB_ERROR,
498 RHYTHMDB_ERROR_ACCESS_FAILED,
499 _("Couldn't access %s: %s"),
500 utf8ised,
501 gnome_vfs_result_to_string (result));
502 rb_debug ("got error on %s: %s", utf8ised, error->message);
503 g_free (unescaped);
504 g_free (utf8ised);
505 return error;
508 static void
509 rhythmdb_execute_multi_stat_info_cb (GnomeVFSAsyncHandle *handle,
510 GList *results,
511 /* GnomeVFSGetFileInfoResult* items */
512 RhythmDB *db)
514 g_mutex_lock (db->priv->stat_mutex);
515 while (results != NULL) {
516 GnomeVFSGetFileInfoResult *info_result = results->data;
517 RhythmDBEvent *event;
519 event = g_hash_table_lookup (db->priv->stat_events, info_result->uri);
520 if (event == NULL) {
521 char *uri_string;
522 uri_string = gnome_vfs_uri_to_string (info_result->uri, GNOME_VFS_URI_HIDE_NONE);
523 rb_debug ("ignoring unexpected uri in gnome_vfs_async_get_file_info response: %s",
524 uri_string);
525 g_free (uri_string);
526 results = results->next;
527 continue;
529 g_hash_table_remove (db->priv->stat_events, info_result->uri);
531 if (info_result->result == GNOME_VFS_OK) {
532 event->vfsinfo = gnome_vfs_file_info_dup (info_result->file_info);
533 } else {
534 event->error = make_access_failed_error (rb_refstring_get (event->real_uri),
535 info_result->result);
536 event->vfsinfo = NULL;
538 g_async_queue_push (db->priv->event_queue, event);
540 results = results->next;
542 db->priv->stat_handle = NULL;
543 g_mutex_unlock (db->priv->stat_mutex);
546 void
547 rhythmdb_start_action_thread (RhythmDB *db)
549 g_mutex_lock (db->priv->stat_mutex);
550 db->priv->action_thread_running = TRUE;
551 rhythmdb_thread_create (db, NULL, (GThreadFunc) action_thread_main, db);
553 if (db->priv->stat_list != NULL) {
554 gnome_vfs_async_get_file_info (&db->priv->stat_handle, db->priv->stat_list,
555 GNOME_VFS_FILE_INFO_FOLLOW_LINKS,
556 GNOME_VFS_PRIORITY_MIN,
557 (GnomeVFSAsyncGetFileInfoCallback) rhythmdb_execute_multi_stat_info_cb,
558 db);
559 g_list_free (db->priv->stat_list);
560 db->priv->stat_list = NULL;
563 g_mutex_unlock (db->priv->stat_mutex);
566 static void
567 rhythmdb_action_free (RhythmDB *db,
568 RhythmDBAction *action)
570 rb_refstring_unref (action->uri);
571 g_free (action);
574 static void
575 rhythmdb_event_free (RhythmDB *db,
576 RhythmDBEvent *result)
578 switch (result->type) {
579 case RHYTHMDB_EVENT_THREAD_EXITED:
580 g_object_unref (db);
581 g_assert (g_atomic_int_dec_and_test (&db->priv->outstanding_threads) >= 0);
582 g_async_queue_unref (db->priv->action_queue);
583 g_async_queue_unref (db->priv->event_queue);
584 break;
585 case RHYTHMDB_EVENT_STAT:
586 case RHYTHMDB_EVENT_METADATA_LOAD:
587 case RHYTHMDB_EVENT_DB_LOAD:
588 case RHYTHMDB_EVENT_DB_SAVED:
589 case RHYTHMDB_EVENT_QUERY_COMPLETE:
590 case RHYTHMDB_EVENT_FILE_CREATED_OR_MODIFIED:
591 case RHYTHMDB_EVENT_FILE_DELETED:
592 break;
593 case RHYTHMDB_EVENT_ENTRY_SET:
594 g_value_unset (&result->change.new);
595 break;
597 if (result->error)
598 g_error_free (result->error);
599 rb_refstring_unref (result->uri);
600 rb_refstring_unref (result->real_uri);
601 if (result->vfsinfo)
602 gnome_vfs_file_info_unref (result->vfsinfo);
603 if (result->metadata)
604 g_object_unref (result->metadata);
605 if (result->results)
606 g_object_unref (result->results);
607 if (result->handle)
608 gnome_vfs_async_cancel (result->handle);
609 if (result->entry != NULL) {
610 rhythmdb_entry_unref (result->entry);
612 g_free (result);
616 * rhythmdb_shutdown:
618 * Ceases all #RhythmDB operations, including stopping all directory monitoring, and
619 * removing all actions and events currently queued.
621 static void
622 _shutdown_foreach_swapped (RhythmDBEvent *event, RhythmDB *db)
624 rhythmdb_event_free (db, event);
627 void
628 rhythmdb_shutdown (RhythmDB *db)
630 RhythmDBEvent *result;
631 RhythmDBAction *action;
633 g_return_if_fail (RHYTHMDB_IS (db));
635 db->priv->exiting = TRUE;
637 eel_gconf_notification_remove (db->priv->library_location_notify_id);
638 g_slist_foreach (db->priv->library_locations, (GFunc) g_free, NULL);
639 g_slist_free (db->priv->library_locations);
640 db->priv->library_locations = NULL;
642 /* abort all async vfs operations */
643 g_mutex_lock (db->priv->stat_mutex);
644 if (db->priv->stat_handle) {
645 gnome_vfs_async_cancel (db->priv->stat_handle);
646 db->priv->stat_handle = NULL;
648 g_list_foreach (db->priv->outstanding_stats, (GFunc)_shutdown_foreach_swapped, db);
649 g_list_free (db->priv->outstanding_stats);
650 db->priv->outstanding_stats = NULL;
651 g_mutex_unlock (db->priv->stat_mutex);
653 while ((action = g_async_queue_try_pop (db->priv->action_queue)) != NULL) {
654 rhythmdb_action_free (db, action);
657 rb_debug ("%d outstanding threads", g_atomic_int_get (&db->priv->outstanding_threads));
658 while (g_atomic_int_get (&db->priv->outstanding_threads) > 0) {
659 result = g_async_queue_pop (db->priv->event_queue);
660 rhythmdb_event_free (db, result);
663 /* FIXME */
664 while ((result = g_async_queue_try_pop (db->priv->event_queue)) != NULL)
665 rhythmdb_event_free (db, result);
668 static void
669 rhythmdb_finalize (GObject *object)
671 RhythmDB *db;
672 int i;
674 g_return_if_fail (object != NULL);
675 g_return_if_fail (RHYTHMDB_IS (object));
677 db = RHYTHMDB (object);
679 g_return_if_fail (db->priv != NULL);
681 rhythmdb_finalize_monitoring (db);
683 g_source_remove (db->priv->event_poll_id);
684 if (db->priv->save_timeout_id > 0)
685 g_source_remove (db->priv->save_timeout_id);
686 if (db->priv->emit_entry_signals_id > 0) {
687 g_source_remove (db->priv->emit_entry_signals_id);
688 g_list_foreach (db->priv->added_entries_to_emit, (GFunc)rhythmdb_entry_unref, NULL);
689 g_list_foreach (db->priv->deleted_entries_to_emit, (GFunc)rhythmdb_entry_unref, NULL);
692 g_thread_pool_free (db->priv->query_thread_pool, FALSE, TRUE);
693 g_thread_pool_free (db->priv->add_thread_pool, FALSE, TRUE);
694 g_async_queue_unref (db->priv->action_queue);
695 g_async_queue_unref (db->priv->event_queue);
696 g_async_queue_unref (db->priv->restored_queue);
698 g_mutex_free (db->priv->saving_mutex);
699 g_cond_free (db->priv->saving_condition);
701 g_hash_table_destroy (db->priv->stat_events);
702 g_mutex_free (db->priv->stat_mutex);
704 g_mutex_free (db->priv->change_mutex);
706 g_hash_table_destroy (db->priv->propname_map);
708 g_hash_table_destroy (db->priv->added_entries);
709 g_hash_table_destroy (db->priv->deleted_entries);
710 g_hash_table_destroy (db->priv->changed_entries);
712 rb_refstring_unref (db->priv->empty_string);
713 rb_refstring_unref (db->priv->octet_stream_str);
715 g_hash_table_destroy (db->priv->entry_type_map);
716 g_mutex_free (db->priv->entry_type_map_mutex);
717 g_mutex_free (db->priv->entry_type_mutex);
719 g_object_unref (db->priv->metadata);
721 for (i = 0; i < RHYTHMDB_NUM_PROPERTIES; i++) {
722 xmlFree (db->priv->column_xml_names[i]);
724 g_free (db->priv->column_xml_names);
726 g_free (db->priv->name);
728 G_OBJECT_CLASS (rhythmdb_parent_class)->finalize (object);
731 static void
732 rhythmdb_set_property (GObject *object,
733 guint prop_id,
734 const GValue *value,
735 GParamSpec *pspec)
737 RhythmDB *source = RHYTHMDB (object);
739 switch (prop_id) {
740 case PROP_NAME:
741 source->priv->name = g_strdup (g_value_get_string (value));
742 break;
743 case PROP_DRY_RUN:
744 source->priv->dry_run = g_value_get_boolean (value);
745 break;
746 case PROP_NO_UPDATE:
747 source->priv->no_update = g_value_get_boolean (value);
748 break;
749 default:
750 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
751 break;
755 static void
756 rhythmdb_get_property (GObject *object,
757 guint prop_id,
758 GValue *value,
759 GParamSpec *pspec)
761 RhythmDB *source = RHYTHMDB (object);
763 switch (prop_id) {
764 case PROP_NAME:
765 g_value_set_string (value, source->priv->name);
766 break;
767 case PROP_DRY_RUN:
768 g_value_set_boolean (value, source->priv->dry_run);
769 break;
770 case PROP_NO_UPDATE:
771 g_value_set_boolean (value, source->priv->no_update);
772 break;
773 default:
774 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
775 break;
779 static void
780 rhythmdb_thread_create (RhythmDB *db,
781 GThreadPool *pool,
782 GThreadFunc func,
783 gpointer data)
785 g_object_ref (db);
786 g_atomic_int_inc (&db->priv->outstanding_threads);
787 g_async_queue_ref (db->priv->action_queue);
788 g_async_queue_ref (db->priv->event_queue);
790 if (pool)
791 g_thread_pool_push (pool, data, NULL);
792 else
793 g_thread_create ((GThreadFunc) func, data, FALSE, NULL);
796 static gboolean
797 rhythmdb_get_readonly (RhythmDB *db)
799 return (g_atomic_int_get (&db->priv->read_counter) > 0);
802 static void
803 rhythmdb_read_enter (RhythmDB *db)
805 gint count;
806 g_return_if_fail (g_atomic_int_get (&db->priv->read_counter) >= 0);
807 g_assert (rb_is_main_thread ());
809 count = g_atomic_int_exchange_and_add (&db->priv->read_counter, 1);
810 rb_debug ("counter: %d", count+1);
811 if (count == 0)
812 g_signal_emit (G_OBJECT (db), rhythmdb_signals[READ_ONLY],
813 0, TRUE);
816 static void
817 rhythmdb_read_leave (RhythmDB *db)
819 gint count;
820 g_return_if_fail (rhythmdb_get_readonly (db));
821 g_assert (rb_is_main_thread ());
823 count = g_atomic_int_exchange_and_add (&db->priv->read_counter, -1);
824 rb_debug ("counter: %d", count-1);
825 if (count == 1)
826 g_signal_emit (G_OBJECT (db), rhythmdb_signals[READ_ONLY],
827 0, FALSE);
830 static gboolean
831 free_entry_changes (RhythmDBEntry *entry,
832 GSList *changes,
833 RhythmDB *db)
835 GSList *t;
836 for (t = changes; t; t = t->next) {
837 RhythmDBEntryChange *change = t->data;
838 g_value_unset (&change->old);
839 g_value_unset (&change->new);
840 g_free (change);
842 g_slist_free (changes);
844 return TRUE;
847 static void
848 emit_entry_changed (RhythmDBEntry *entry,
849 GSList *changes,
850 RhythmDB *db)
852 g_signal_emit (G_OBJECT (db), rhythmdb_signals[ENTRY_CHANGED], 0, entry, changes);
855 static void
856 sync_entry_changed (RhythmDBEntry *entry,
857 GSList *changes,
858 RhythmDB *db)
860 GSList *t;
862 for (t = changes; t; t = t->next) {
863 RBMetaDataField field;
864 RhythmDBEntryChange *change = t->data;
866 if (metadata_field_from_prop (change->prop, &field)) {
867 RhythmDBAction *action;
869 if (!rhythmdb_entry_is_editable (db, entry)) {
870 g_warning ("trying to sync properties of non-editable file");
871 break;
874 action = g_new0 (RhythmDBAction, 1);
875 action->type = RHYTHMDB_ACTION_SYNC;
876 action->uri = rb_refstring_ref (entry->location);
877 g_async_queue_push (db->priv->action_queue, action);
878 break;
883 static gboolean
884 rhythmdb_emit_entry_signals_idle (RhythmDB *db)
886 GList *added_entries;
887 GList *deleted_entries;
888 GList *l;
890 /* get lists of entries to emit, reset source id value */
891 g_mutex_lock (db->priv->change_mutex);
893 added_entries = db->priv->added_entries_to_emit;
894 db->priv->added_entries_to_emit = NULL;
896 deleted_entries = db->priv->deleted_entries_to_emit;
897 db->priv->deleted_entries_to_emit = NULL;
899 db->priv->emit_entry_signals_id = 0;
901 g_mutex_unlock (db->priv->change_mutex);
903 /* emit added entries */
904 for (l = added_entries; l; l = g_list_next (l)) {
905 RhythmDBEntry *entry = (RhythmDBEntry *)l->data;
906 g_signal_emit (G_OBJECT (db), rhythmdb_signals[ENTRY_ADDED], 0, entry);
907 rhythmdb_entry_unref (entry);
910 /* emit deleted entries */
911 for (l = deleted_entries; l; l = g_list_next (l)) {
912 RhythmDBEntry *entry = (RhythmDBEntry *)l->data;
913 g_signal_emit (G_OBJECT (db), rhythmdb_signals[ENTRY_DELETED], 0, entry);
914 rhythmdb_entry_unref (entry);
917 g_list_free (added_entries);
918 g_list_free (deleted_entries);
919 return FALSE;
922 static gboolean
923 process_added_entries_cb (RhythmDBEntry *entry,
924 GThread *thread,
925 RhythmDB *db)
927 if (thread != g_thread_self ())
928 return FALSE;
930 if (entry->type == RHYTHMDB_ENTRY_TYPE_SONG) {
931 const gchar *uri;
933 uri = rhythmdb_entry_get_string (entry,
934 RHYTHMDB_PROP_LOCATION);
936 #ifdef HAVE_GSTREAMER_0_8
937 /* always start remote files hidden*/
938 if (!g_str_has_prefix (uri, "file://")) {
939 entry->flags |= RHYTHMDB_ENTRY_HIDDEN;
941 #endif
943 queue_stat_uri (uri, db, RHYTHMDB_ENTRY_TYPE_INVALID);
946 g_assert ((entry->flags & RHYTHMDB_ENTRY_INSERTED) == 0);
947 entry->flags |= RHYTHMDB_ENTRY_INSERTED;
949 rhythmdb_entry_ref (entry);
950 db->priv->added_entries_to_emit = g_list_prepend (db->priv->added_entries_to_emit, entry);
952 return TRUE;
955 static gboolean
956 process_deleted_entries_cb (RhythmDBEntry *entry,
957 GThread *thread,
958 RhythmDB *db)
960 if (thread != g_thread_self ())
961 return FALSE;
963 rhythmdb_entry_ref (entry);
964 db->priv->deleted_entries_to_emit = g_list_prepend (db->priv->deleted_entries_to_emit, entry);
966 return TRUE;
969 static void
970 rhythmdb_commit_internal (RhythmDB *db,
971 gboolean sync_changes,
972 GThread *thread)
974 g_mutex_lock (db->priv->change_mutex);
976 g_hash_table_foreach (db->priv->changed_entries, (GHFunc) emit_entry_changed, db);
977 if (sync_changes)
978 g_hash_table_foreach (db->priv->changed_entries, (GHFunc) sync_entry_changed, db);
979 g_hash_table_foreach_remove (db->priv->changed_entries, (GHRFunc) free_entry_changes, db);
981 /* update the lists of entry added/deleted signals to emit */
982 g_hash_table_foreach_remove (db->priv->added_entries, (GHRFunc) process_added_entries_cb, db);
983 g_hash_table_foreach_remove (db->priv->deleted_entries, (GHRFunc) process_deleted_entries_cb, db);
985 /* if there are some signals to emit, add a new idle callback if required */
986 if (db->priv->added_entries_to_emit || db->priv->deleted_entries_to_emit) {
987 if (db->priv->emit_entry_signals_id == 0)
988 db->priv->emit_entry_signals_id = g_idle_add ((GSourceFunc) rhythmdb_emit_entry_signals_idle, db);
991 g_mutex_unlock (db->priv->change_mutex);
994 typedef struct {
995 RhythmDB *db;
996 gboolean sync;
997 GThread *thread;
998 } RhythmDBTimeoutCommitData;
1000 static gboolean
1001 timeout_rhythmdb_commit (RhythmDBTimeoutCommitData *data)
1003 rhythmdb_commit_internal (data->db, data->sync, data->thread);
1004 g_object_unref (data->db);
1005 g_free (data);
1006 return FALSE;
1009 static void
1010 rhythmdb_add_timeout_commit (RhythmDB *db,
1011 gboolean sync)
1013 RhythmDBTimeoutCommitData *data;
1015 g_assert (rb_is_main_thread ());
1017 data = g_new0 (RhythmDBTimeoutCommitData, 1);
1018 data->db = g_object_ref (db);
1019 data->sync = sync;
1020 data->thread = g_thread_self ();
1021 g_timeout_add (100, (GSourceFunc)timeout_rhythmdb_commit, data);
1025 * rhythmdb_commit:
1026 * @db: a #RhythmDB.
1028 * Apply all database changes, and send notification of changes and new entries.
1029 * This needs to be called after any changes have been made, such as a group of
1030 * rhythmdb_entry_set() calls, or a new entry has been added.
1032 void
1033 rhythmdb_commit (RhythmDB *db)
1035 rhythmdb_commit_internal (db, TRUE, g_thread_self ());
1038 GQuark
1039 rhythmdb_error_quark (void)
1041 static GQuark quark;
1042 if (!quark)
1043 quark = g_quark_from_static_string ("rhythmdb_error");
1045 return quark;
1048 /* structure alignment magic, stolen from glib */
1049 #define STRUCT_ALIGNMENT (2 * sizeof (gsize))
1050 #define ALIGN_STRUCT(offset) \
1051 ((offset + (STRUCT_ALIGNMENT - 1)) & -STRUCT_ALIGNMENT)
1054 * rhythmdb_entry_allocate:
1055 * @db: a #RhythmDB.
1056 * @type: type of entry to allocate
1058 * Allocate and initialise memory for a new #RhythmDBEntry of the type @type.
1059 * The entry's initial properties needs to be set with rhythmdb_entry_set (),
1060 * the entry added to the database with rhythmdb_entry_insert(), and committed with
1061 * rhythmdb_commit().
1063 * This should only be used by RhythmDB itself, or a backend (such as rhythmdb-tree).
1065 * Returns: the newly allocated #RhythmDBEntry
1067 RhythmDBEntry *
1068 rhythmdb_entry_allocate (RhythmDB *db,
1069 RhythmDBEntryType type)
1071 RhythmDBEntry *ret;
1072 gsize size = sizeof (RhythmDBEntry);
1074 if (type->entry_type_data_size) {
1075 size = ALIGN_STRUCT (sizeof (RhythmDBEntry)) + type->entry_type_data_size;
1077 ret = g_malloc0 (size);
1078 ret->id = (guint) g_atomic_int_exchange_and_add (&db->priv->next_entry_id, 1);
1080 ret->type = type;
1081 ret->title = rb_refstring_ref (db->priv->empty_string);
1082 ret->genre = rb_refstring_ref (db->priv->empty_string);
1083 ret->artist = rb_refstring_ref (db->priv->empty_string);
1084 ret->album = rb_refstring_ref (db->priv->empty_string);
1085 ret->musicbrainz_trackid = rb_refstring_ref (db->priv->empty_string);
1086 ret->mimetype = rb_refstring_ref (db->priv->octet_stream_str);
1088 ret->flags |= RHYTHMDB_ENTRY_LAST_PLAYED_DIRTY |
1089 RHYTHMDB_ENTRY_FIRST_SEEN_DIRTY |
1090 RHYTHMDB_ENTRY_LAST_SEEN_DIRTY;
1092 /* The refcount is initially 0, we want to set it to 1 */
1093 ret->refcount = 1;
1095 if (type->post_entry_create)
1096 (type->post_entry_create)(ret, type->post_entry_create_data);
1098 return ret;
1102 * rhythmdb_entry_get_type_data:
1103 * @entry: a #RhythmDBEntry
1104 * @expected_size: expected size of the type-specific data.
1106 * Returns a pointer to the entry's type-specific data, checking that
1107 * the size of the data structure matches what is expected.
1108 * Callers should use the RHYTHMDB_ENTRY_GET_TYPE_DATA macro for
1109 * a slightly more friendly interface to this functionality.
1111 gpointer
1112 rhythmdb_entry_get_type_data (RhythmDBEntry *entry,
1113 guint expected_size)
1115 g_return_val_if_fail (entry != NULL, NULL);
1117 g_assert (expected_size == entry->type->entry_type_data_size);
1118 gsize offset = ALIGN_STRUCT (sizeof (RhythmDBEntry));
1120 return (gpointer) (((guint8 *)entry) + offset);
1124 * rhythmdb_entry_insert:
1125 * @db: a #RhythmDB.
1126 * @entry: the entry to insert.
1128 * Inserts a newly-created entry into the database.
1130 * Note that you must call rhythmdb_commit() at some point after invoking
1131 * this function.
1133 void
1134 rhythmdb_entry_insert (RhythmDB *db,
1135 RhythmDBEntry *entry)
1137 g_return_if_fail (RHYTHMDB_IS (db));
1138 g_return_if_fail (entry != NULL);
1140 g_assert ((entry->flags & RHYTHMDB_ENTRY_INSERTED) == 0);
1141 g_return_if_fail (entry->location != NULL);
1143 /* ref the entry before adding to hash, it is unreffed when removed */
1144 rhythmdb_entry_ref (entry);
1145 g_mutex_lock (db->priv->change_mutex);
1146 g_hash_table_insert (db->priv->added_entries, entry, g_thread_self ());
1147 g_mutex_unlock (db->priv->change_mutex);
1151 * rhythmdb_entry_new:
1152 * @db: a #RhythmDB.
1153 * @type: type of entry to create
1154 * @uri: the location of the entry, this be unique amongst all entries.
1156 * Creates a new entry of type @type and location @uri, and inserts
1157 * it into the database. You must call rhythmdb_commit() at some point
1158 * after invoking this function.
1160 * This may return NULL if entry creation fails. This can occur if there is
1161 * already an entry with the given uri.
1163 * Returns: the newly created #RhythmDBEntry
1165 RhythmDBEntry *
1166 rhythmdb_entry_new (RhythmDB *db,
1167 RhythmDBEntryType type,
1168 const char *uri)
1170 RhythmDBEntry *ret;
1171 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
1173 ret = rhythmdb_entry_lookup_by_location (db, uri);
1174 if (ret) {
1175 g_warning ("attempting to create entry that already exists: %s", uri);
1176 return NULL;
1179 ret = rhythmdb_entry_allocate (db, type);
1180 ret->location = rb_refstring_new (uri);
1181 klass->impl_entry_new (db, ret);
1182 rb_debug ("emitting entry added");
1183 rhythmdb_entry_insert (db, ret);
1185 return ret;
1189 * rhythmdb_entry_example_new:
1190 * @db: a #RhythmDB.
1191 * @type: type of entry to create
1192 * @uri: the location of the entry, this be unique amongst all entries.
1194 * Creates a new sample entry of type @type and location @uri, it does not insert
1195 * it into the database. This is indended for use as a example entry.
1197 * This may return NULL if entry creation fails.
1199 * Returns: the newly created #RhythmDBEntry
1201 RhythmDBEntry *
1202 rhythmdb_entry_example_new (RhythmDB *db,
1203 RhythmDBEntryType type,
1204 const char *uri)
1206 RhythmDBEntry *ret;
1208 ret = rhythmdb_entry_allocate (db, type);
1209 if (uri)
1210 ret->location = rb_refstring_new (uri);
1212 if (type == RHYTHMDB_ENTRY_TYPE_SONG) {
1213 rb_refstring_unref (ret->artist);
1214 ret->artist = rb_refstring_new ("The Beatles");
1215 rb_refstring_unref (ret->album);
1216 ret->album = rb_refstring_new ("Help!");
1217 rb_refstring_unref (ret->title);
1218 ret->title = rb_refstring_new ("Ticket To Ride");
1219 ret->tracknum = 7;
1220 } else {
1223 return ret;
1227 * rhythmdb_entry_ref:
1228 * @db: a #RhythmDB.
1229 * @entry: a #RhythmDBEntry.
1231 * Increase the reference count of the entry.
1233 RhythmDBEntry *
1234 rhythmdb_entry_ref (RhythmDBEntry *entry)
1236 g_return_val_if_fail (entry != NULL, NULL);
1237 g_return_val_if_fail (entry->refcount > 0, NULL);
1239 g_atomic_int_inc (&entry->refcount);
1241 return entry;
1244 static void
1245 rhythmdb_entry_finalize (RhythmDBEntry *entry)
1247 RhythmDBEntryType type;
1249 type = rhythmdb_entry_get_entry_type (entry);
1251 if (type->pre_entry_destroy)
1252 (type->pre_entry_destroy)(entry, type->pre_entry_destroy_data);
1254 rb_refstring_unref (entry->location);
1255 rb_refstring_unref (entry->playback_error);
1256 rb_refstring_unref (entry->title);
1257 rb_refstring_unref (entry->genre);
1258 rb_refstring_unref (entry->artist);
1259 rb_refstring_unref (entry->album);
1260 rb_refstring_unref (entry->musicbrainz_trackid);
1261 rb_refstring_unref (entry->mimetype);
1263 g_free (entry);
1267 * rhythmdb_entry_unref:
1268 * @db: a #RhythmDB.
1269 * @entry: a #RhythmDBEntry.
1271 * Decrease the reference count of the entry, and destroy it if there are
1272 * no references left.
1274 void
1275 rhythmdb_entry_unref (RhythmDBEntry *entry)
1277 gboolean is_zero;
1279 g_return_if_fail (entry != NULL);
1280 g_return_if_fail (entry->refcount > 0);
1282 is_zero = g_atomic_int_dec_and_test (&entry->refcount);
1283 if (G_UNLIKELY (is_zero)) {
1284 rhythmdb_entry_finalize (entry);
1289 * rhythmdb_entry_is_editable:
1290 * @db: a #RhythmDB.
1291 * @entry: a #RhythmDBEntry.
1293 * This determines whether any changes to the entries metadata can be saved.
1294 * Usually this is only true for entries backed by files, where tag-writing is
1295 * enabled, and the appropriate tag-writing facilities are available.
1297 * Returns: whether the entries metadata can be changed.
1300 gboolean
1301 rhythmdb_entry_is_editable (RhythmDB *db,
1302 RhythmDBEntry *entry)
1304 RhythmDBEntryType entry_type;
1306 g_return_val_if_fail (RHYTHMDB_IS (db), FALSE);
1307 g_return_val_if_fail (entry != NULL, FALSE);
1309 entry_type = rhythmdb_entry_get_entry_type (entry);
1310 return entry_type->can_sync_metadata (db, entry, entry_type->can_sync_metadata_data);
1313 static void
1314 set_metadata_string_default_unknown (RhythmDB *db,
1315 RBMetaData *metadata,
1316 RhythmDBEntry *entry,
1317 RBMetaDataField field,
1318 RhythmDBPropType prop)
1320 const char *unknown = _("Unknown");
1321 GValue val = {0, };
1323 if (!(rb_metadata_get (metadata,
1324 field,
1325 &val))) {
1326 g_value_init (&val, G_TYPE_STRING);
1327 g_value_set_static_string (&val, unknown);
1328 } else {
1329 const gchar *str = g_value_get_string (&val);
1330 if (str == NULL || str[0] == '\0')
1331 g_value_set_static_string (&val, unknown);
1333 rhythmdb_entry_set_internal (db, entry, TRUE, prop, &val);
1334 g_value_unset (&val);
1337 static void
1338 set_props_from_metadata (RhythmDB *db,
1339 RhythmDBEntry *entry,
1340 GnomeVFSFileInfo *vfsinfo,
1341 RBMetaData *metadata)
1343 const char *mime;
1344 GValue val = {0,};
1346 g_value_init (&val, G_TYPE_STRING);
1347 mime = rb_metadata_get_mime (metadata);
1348 if (mime) {
1349 g_value_set_string (&val, mime);
1350 rhythmdb_entry_set_internal (db, entry, TRUE,
1351 RHYTHMDB_PROP_MIMETYPE, &val);
1353 g_value_unset (&val);
1355 /* track number */
1356 if (!rb_metadata_get (metadata,
1357 RB_METADATA_FIELD_TRACK_NUMBER,
1358 &val)) {
1359 g_value_init (&val, G_TYPE_ULONG);
1360 g_value_set_ulong (&val, 0);
1362 rhythmdb_entry_set_internal (db, entry, TRUE,
1363 RHYTHMDB_PROP_TRACK_NUMBER, &val);
1364 g_value_unset (&val);
1366 /* disc number */
1367 if (!rb_metadata_get (metadata,
1368 RB_METADATA_FIELD_DISC_NUMBER,
1369 &val)) {
1370 g_value_init (&val, G_TYPE_ULONG);
1371 g_value_set_ulong (&val, 0);
1373 rhythmdb_entry_set_internal (db, entry, TRUE,
1374 RHYTHMDB_PROP_DISC_NUMBER, &val);
1375 g_value_unset (&val);
1377 /* duration */
1378 if (rb_metadata_get (metadata,
1379 RB_METADATA_FIELD_DURATION,
1380 &val)) {
1381 rhythmdb_entry_set_internal (db, entry, TRUE,
1382 RHYTHMDB_PROP_DURATION, &val);
1383 g_value_unset (&val);
1386 /* bitrate */
1387 if (rb_metadata_get (metadata,
1388 RB_METADATA_FIELD_BITRATE,
1389 &val)) {
1390 rhythmdb_entry_set_internal (db, entry, TRUE,
1391 RHYTHMDB_PROP_BITRATE, &val);
1392 g_value_unset (&val);
1395 /* date */
1396 if (rb_metadata_get (metadata,
1397 RB_METADATA_FIELD_DATE,
1398 &val)) {
1399 rhythmdb_entry_set_internal (db, entry, TRUE,
1400 RHYTHMDB_PROP_DATE, &val);
1401 g_value_unset (&val);
1404 /* musicbrainz trackid */
1405 if (rb_metadata_get (metadata,
1406 RB_METADATA_FIELD_MUSICBRAINZ_TRACKID,
1407 &val)) {
1408 rhythmdb_entry_set_internal (db, entry, TRUE,
1409 RHYTHMDB_PROP_MUSICBRAINZ_TRACKID, &val);
1410 g_value_unset (&val);
1413 /* filesize */
1414 g_value_init (&val, G_TYPE_UINT64);
1415 g_value_set_uint64 (&val, vfsinfo->size);
1416 rhythmdb_entry_set_internal (db, entry, TRUE, RHYTHMDB_PROP_FILE_SIZE, &val);
1417 g_value_unset (&val);
1419 /* title */
1420 if (!rb_metadata_get (metadata,
1421 RB_METADATA_FIELD_TITLE,
1422 &val) || g_value_get_string (&val)[0] == '\0') {
1423 char *utf8name;
1424 utf8name = g_filename_to_utf8 (vfsinfo->name, -1, NULL, NULL, NULL);
1425 if (!utf8name) {
1426 utf8name = g_strdup (_("<invalid filename>"));
1428 if (G_VALUE_HOLDS_STRING (&val))
1429 g_value_reset (&val);
1430 else
1431 g_value_init (&val, G_TYPE_STRING);
1432 g_value_set_string (&val, utf8name);
1433 g_free (utf8name);
1435 rhythmdb_entry_set_internal (db, entry, TRUE, RHYTHMDB_PROP_TITLE, &val);
1436 g_value_unset (&val);
1438 /* genre */
1439 set_metadata_string_default_unknown (db, metadata, entry,
1440 RB_METADATA_FIELD_GENRE,
1441 RHYTHMDB_PROP_GENRE);
1443 /* artist */
1444 set_metadata_string_default_unknown (db, metadata, entry,
1445 RB_METADATA_FIELD_ARTIST,
1446 RHYTHMDB_PROP_ARTIST);
1447 /* album */
1448 set_metadata_string_default_unknown (db, metadata, entry,
1449 RB_METADATA_FIELD_ALBUM,
1450 RHYTHMDB_PROP_ALBUM);
1452 /* replaygain track gain */
1453 if (rb_metadata_get (metadata,
1454 RB_METADATA_FIELD_TRACK_GAIN,
1455 &val)) {
1456 rhythmdb_entry_set_internal (db, entry, TRUE,
1457 RHYTHMDB_PROP_TRACK_GAIN, &val);
1458 g_value_unset (&val);
1461 /* replaygain track peak */
1462 if (rb_metadata_get (metadata,
1463 RB_METADATA_FIELD_TRACK_PEAK,
1464 &val)) {
1465 rhythmdb_entry_set_internal (db, entry, TRUE,
1466 RHYTHMDB_PROP_TRACK_PEAK, &val);
1467 g_value_unset (&val);
1470 /* replaygain album gain */
1471 if (rb_metadata_get (metadata,
1472 RB_METADATA_FIELD_ALBUM_GAIN,
1473 &val)) {
1474 rhythmdb_entry_set_internal (db, entry, TRUE,
1475 RHYTHMDB_PROP_ALBUM_GAIN, &val);
1476 g_value_unset (&val);
1479 /* replaygain album peak */
1480 if (rb_metadata_get (metadata,
1481 RB_METADATA_FIELD_ALBUM_PEAK,
1482 &val)) {
1483 rhythmdb_entry_set_internal (db, entry, TRUE,
1484 RHYTHMDB_PROP_ALBUM_PEAK, &val);
1485 g_value_unset (&val);
1489 static gboolean
1490 is_ghost_entry (RhythmDBEntry *entry)
1492 GTimeVal time;
1493 gulong last_seen;
1494 gulong grace_period;
1495 GError *error;
1496 GConfClient *client;
1498 client = gconf_client_get_default ();
1499 if (client == NULL) {
1500 return FALSE;
1502 error = NULL;
1503 grace_period = gconf_client_get_int (client, CONF_GRACE_PERIOD,
1504 &error);
1505 g_object_unref (G_OBJECT (client));
1506 if (error != NULL) {
1507 g_error_free (error);
1508 return FALSE;
1511 /* This is a bit silly, but I prefer to make sure we won't
1512 * overflow in the following calculations
1514 if ((grace_period < 0) || (grace_period > 20000)) {
1515 return FALSE;
1518 /* Convert from days to seconds */
1519 grace_period = grace_period * 60 * 60 * 24;
1520 g_get_current_time (&time);
1521 last_seen = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_LAST_SEEN);
1523 return (last_seen + grace_period < time.tv_sec);
1526 static void
1527 rhythmdb_process_stat_event (RhythmDB *db,
1528 RhythmDBEvent *event)
1530 RhythmDBEntry *entry;
1531 RhythmDBAction *action;
1533 entry = rhythmdb_entry_lookup_by_location_refstring (db, event->real_uri);
1534 if (entry) {
1535 time_t mtime = (time_t) entry->mtime;
1537 if ((event->entry_type != RHYTHMDB_ENTRY_TYPE_INVALID) && (entry->type != event->entry_type))
1538 g_warning ("attempt to use same location in multiple entry types");
1540 if (entry->type == RHYTHMDB_ENTRY_TYPE_IGNORE)
1541 rb_debug ("ignoring %p", entry);
1543 if (event->error) {
1544 if (!is_ghost_entry (entry)) {
1545 rhythmdb_entry_set_visibility (db, entry, FALSE);
1546 } else {
1547 rb_debug ("error accessing %s: %s", rb_refstring_get (event->real_uri),
1548 event->error->message);
1549 rhythmdb_entry_delete (db, entry);
1551 } else {
1552 GValue val = {0, };
1553 GTimeVal time;
1554 const char *mount_point;
1556 rhythmdb_entry_set_visibility (db, entry, TRUE);
1558 /* Update mount point if necessary (main reason is
1559 * that we want to set the mount point in legacy
1560 * rhythmdb that doesn't have it already
1562 mount_point = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MOUNTPOINT);
1563 if (mount_point == NULL) {
1564 rhythmdb_entry_set_mount_point (db, entry,
1565 rb_refstring_get (event->real_uri));
1568 /* Update last seen time. It will also be updated
1569 * upon saving and when a volume is unmounted
1571 g_get_current_time (&time);
1572 g_value_init (&val, G_TYPE_ULONG);
1573 g_value_set_ulong (&val, time.tv_sec);
1574 rhythmdb_entry_set_internal (db, entry, TRUE,
1575 RHYTHMDB_PROP_LAST_SEEN,
1576 &val);
1577 /* Old rhythmdb.xml files won't have a value for
1578 * FIRST_SEEN, so set it here
1580 if (rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_FIRST_SEEN) == 0) {
1581 rhythmdb_entry_set_internal (db, entry, TRUE,
1582 RHYTHMDB_PROP_FIRST_SEEN,
1583 &val);
1585 g_value_unset (&val);
1587 if (mtime == event->vfsinfo->mtime) {
1588 rb_debug ("not modified: %s", rb_refstring_get (event->real_uri));
1589 /* monitor the file for changes */
1590 if (eel_gconf_get_boolean (CONF_MONITOR_LIBRARY))
1591 rhythmdb_monitor_uri_path (db, rb_refstring_get (entry->location), NULL /* FIXME */);
1592 } else {
1593 RhythmDBEvent *new_event;
1595 rb_debug ("changed: %s", rb_refstring_get (event->real_uri));
1596 new_event = g_new0 (RhythmDBEvent, 1);
1597 new_event->db = db;
1598 new_event->uri = rb_refstring_ref (event->real_uri);
1599 new_event->type = RHYTHMDB_EVENT_FILE_CREATED_OR_MODIFIED;
1600 g_async_queue_push (db->priv->event_queue,
1601 new_event);
1605 rhythmdb_commit (db);
1606 } else {
1607 action = g_new0 (RhythmDBAction, 1);
1608 action->type = RHYTHMDB_ACTION_LOAD;
1609 action->uri = rb_refstring_ref (event->real_uri);
1610 action->entry_type = event->entry_type;
1611 rb_debug ("queuing a RHYTHMDB_ACTION_LOAD: %s", rb_refstring_get (action->uri));
1612 g_async_queue_push (db->priv->action_queue, action);
1616 typedef struct
1618 RhythmDB *db;
1619 char *uri;
1620 char *msg;
1621 } RhythmDBLoadErrorData;
1623 static void
1624 rhythmdb_add_import_error_entry (RhythmDB *db,
1625 RhythmDBEvent *event)
1627 RhythmDBEntry *entry;
1628 GValue value = {0,};
1629 RhythmDBEntryType error_entry_type = RHYTHMDB_ENTRY_TYPE_IMPORT_ERROR;
1631 if (g_error_matches (event->error, RB_METADATA_ERROR, RB_METADATA_ERROR_NOT_AUDIO_IGNORE)) {
1632 /* only add an ignore entry for the main library */
1633 if (event->entry_type != RHYTHMDB_ENTRY_TYPE_SONG &&
1634 event->entry_type != RHYTHMDB_ENTRY_TYPE_INVALID)
1635 return;
1637 error_entry_type = RHYTHMDB_ENTRY_TYPE_IGNORE;
1640 entry = rhythmdb_entry_lookup_by_location_refstring (db, event->real_uri);
1641 if (entry) {
1642 RhythmDBEntryType entry_type = rhythmdb_entry_get_entry_type (entry);
1643 if (entry_type != RHYTHMDB_ENTRY_TYPE_IMPORT_ERROR &&
1644 entry_type != RHYTHMDB_ENTRY_TYPE_IGNORE) {
1645 /* FIXME we've successfully read this file before.. so what should we do? */
1646 rb_debug ("%s already exists in the library.. ignoring import error?", rb_refstring_get (event->real_uri));
1647 return;
1650 if (entry_type != error_entry_type) {
1651 /* delete the existing entry, then create a new one below */
1652 rhythmdb_entry_delete (db, entry);
1653 entry = NULL;
1654 } else if (error_entry_type == RHYTHMDB_ENTRY_TYPE_IMPORT_ERROR) {
1655 /* we've already got an error for this file, so just update it */
1656 g_value_init (&value, G_TYPE_STRING);
1657 g_value_set_string (&value, event->error->message);
1658 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &value);
1659 g_value_unset (&value);
1660 } else {
1661 /* no need to update the ignored file entry */
1664 if (entry && event->vfsinfo) {
1665 /* mtime */
1666 g_value_init (&value, G_TYPE_ULONG);
1667 g_value_set_ulong (&value, event->vfsinfo->mtime);
1668 rhythmdb_entry_set(db, entry, RHYTHMDB_PROP_MTIME, &value);
1669 g_value_unset (&value);
1672 rhythmdb_add_timeout_commit (db, FALSE);
1675 if (entry == NULL) {
1676 /* create a new import error or ignore entry */
1677 entry = rhythmdb_entry_new (db, error_entry_type, rb_refstring_get (event->real_uri));
1678 if (entry == NULL)
1679 return;
1681 if (error_entry_type == RHYTHMDB_ENTRY_TYPE_IMPORT_ERROR && event->error->message) {
1682 g_value_init (&value, G_TYPE_STRING);
1683 if (g_utf8_validate (event->error->message, -1, NULL))
1684 g_value_set_string (&value, event->error->message);
1685 else
1686 g_value_set_static_string (&value, _("invalid unicode in error message"));
1687 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &value);
1688 g_value_unset (&value);
1691 /* mtime */
1692 if (event->vfsinfo) {
1693 g_value_init (&value, G_TYPE_ULONG);
1694 g_value_set_ulong (&value, event->vfsinfo->mtime);
1695 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_MTIME, &value);
1696 g_value_unset (&value);
1699 /* record the mount point so we can delete entries for unmounted volumes */
1700 rhythmdb_entry_set_mount_point (db, entry, rb_refstring_get (event->real_uri));
1702 rhythmdb_entry_set_visibility (db, entry, TRUE);
1704 rhythmdb_add_timeout_commit (db, FALSE);
1708 static gboolean
1709 rhythmdb_process_metadata_load (RhythmDB *db,
1710 RhythmDBEvent *event)
1712 RhythmDBEntry *entry;
1713 GValue value = {0,};
1714 const char *mime;
1715 GTimeVal time;
1717 if (rhythmdb_get_readonly (db)) {
1718 rb_debug ("database is read-only right now, re-queuing event");
1719 g_async_queue_push (db->priv->event_queue, event);
1720 return FALSE;
1723 if (event->error) {
1724 rhythmdb_add_import_error_entry (db, event);
1725 return TRUE;
1728 /* do we really need to do this? */
1729 mime = rb_metadata_get_mime (event->metadata);
1730 if (!mime) {
1731 rb_debug ("unsupported file");
1732 return TRUE;
1735 g_get_current_time (&time);
1737 entry = rhythmdb_entry_lookup_by_location_refstring (db, event->real_uri);
1739 if (entry != NULL) {
1740 if ((event->entry_type != RHYTHMDB_ENTRY_TYPE_INVALID) &&
1741 (rhythmdb_entry_get_entry_type (entry) != event->entry_type)) {
1742 /* switching from IGNORE to SONG or vice versa, recreate the entry */
1743 rhythmdb_entry_delete (db, entry);
1744 rhythmdb_add_timeout_commit (db, FALSE);
1745 entry = NULL;
1749 if (entry == NULL) {
1750 if (event->entry_type == RHYTHMDB_ENTRY_TYPE_INVALID)
1751 event->entry_type = RHYTHMDB_ENTRY_TYPE_SONG;
1753 entry = rhythmdb_entry_new (db, event->entry_type, rb_refstring_get (event->real_uri));
1754 if (entry == NULL) {
1755 rb_debug ("entry already exists");
1756 return TRUE;
1759 /* initialize the last played date to 0=never */
1760 g_value_init (&value, G_TYPE_ULONG);
1761 g_value_set_ulong (&value, 0);
1762 rhythmdb_entry_set (db, entry,
1763 RHYTHMDB_PROP_LAST_PLAYED, &value);
1764 g_value_unset (&value);
1766 /* initialize the rating */
1767 g_value_init (&value, G_TYPE_DOUBLE);
1768 g_value_set_double (&value, 0);
1769 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_RATING, &value);
1770 g_value_unset (&value);
1772 /* first seen */
1773 g_value_init (&value, G_TYPE_ULONG);
1774 g_value_set_ulong (&value, time.tv_sec);
1775 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_FIRST_SEEN, &value);
1776 g_value_unset (&value);
1779 if ((event->entry_type != RHYTHMDB_ENTRY_TYPE_INVALID) && (entry->type != event->entry_type))
1780 g_warning ("attempt to use same location in multiple entry types");
1782 /* mtime */
1783 if (event->vfsinfo) {
1784 g_value_init (&value, G_TYPE_ULONG);
1785 g_value_set_ulong (&value, event->vfsinfo->mtime);
1786 rhythmdb_entry_set_internal (db, entry, TRUE, RHYTHMDB_PROP_MTIME, &value);
1787 g_value_unset (&value);
1790 if (event->entry_type != RHYTHMDB_ENTRY_TYPE_IGNORE &&
1791 event->entry_type != RHYTHMDB_ENTRY_TYPE_IMPORT_ERROR) {
1792 set_props_from_metadata (db, entry, event->vfsinfo, event->metadata);
1795 /* we've seen this entry */
1796 rhythmdb_entry_set_visibility (db, entry, TRUE);
1798 g_value_init (&value, G_TYPE_ULONG);
1799 g_value_set_ulong (&value, time.tv_sec);
1800 rhythmdb_entry_set_internal (db, entry, TRUE, RHYTHMDB_PROP_LAST_SEEN, &value);
1801 g_value_unset (&value);
1803 /* Remember the mount point of the volume the song is on */
1804 rhythmdb_entry_set_mount_point (db, entry, rb_refstring_get (event->real_uri));
1806 /* monitor the file for changes */
1807 /* FIXME: watch for errors */
1808 if (eel_gconf_get_boolean (CONF_MONITOR_LIBRARY) && event->entry_type == RHYTHMDB_ENTRY_TYPE_SONG)
1809 rhythmdb_monitor_uri_path (db, rb_refstring_get (entry->location), NULL);
1811 rhythmdb_add_timeout_commit (db, FALSE);
1813 return TRUE;
1816 static void
1817 rhythmdb_process_queued_entry_set_event (RhythmDB *db,
1818 RhythmDBEvent *event)
1820 rhythmdb_entry_set_internal (db, event->entry,
1821 event->signal_change,
1822 event->change.prop,
1823 &event->change.new);
1824 /* Don't run rhythmdb_commit right now in case there
1825 * we can run a single commit for several queued
1826 * entry_set
1828 rhythmdb_add_timeout_commit (db, TRUE);
1831 static void
1832 rhythmdb_process_file_created_or_modified (RhythmDB *db,
1833 RhythmDBEvent *event)
1835 RhythmDBAction *action;
1837 action = g_new0 (RhythmDBAction, 1);
1838 action->type = RHYTHMDB_ACTION_LOAD;
1839 action->uri = rb_refstring_ref (event->uri);
1840 action->entry_type = RHYTHMDB_ENTRY_TYPE_INVALID;
1841 g_async_queue_push (db->priv->action_queue, action);
1844 static void
1845 rhythmdb_process_file_deleted (RhythmDB *db,
1846 RhythmDBEvent *event)
1848 RhythmDBEntry *entry = rhythmdb_entry_lookup_by_location_refstring (db, event->uri);
1850 g_hash_table_remove (db->priv->changed_files, event->uri);
1852 if (entry) {
1853 rb_debug ("deleting entry for %s", rb_refstring_get (event->uri));
1854 rhythmdb_entry_set_visibility (db, entry, FALSE);
1855 rhythmdb_commit (db);
1859 static gboolean
1860 rhythmdb_process_events (RhythmDB *db,
1861 GTimeVal *timeout)
1863 RhythmDBEvent *event;
1864 guint count = 0;
1866 while ((event = g_async_queue_try_pop (db->priv->event_queue)) != NULL) {
1867 gboolean free = TRUE;
1869 /* if the database is read-only, we can't process those events
1870 * since they call rhythmdb_entry_set. Doing it this way
1871 * is safe if we assume all calls to read_enter/read_leave
1872 * are done from the main thread (the thread this function
1873 * runs in).
1875 if (rhythmdb_get_readonly (db) &&
1876 ((event->type == RHYTHMDB_EVENT_STAT)
1877 || (event->type == RHYTHMDB_EVENT_METADATA_LOAD)
1878 || (event->type == RHYTHMDB_EVENT_ENTRY_SET))) {
1879 if (count >= g_async_queue_length (db->priv->event_queue)) {
1880 rb_debug ("Database is read-only, and we can't process any more events");
1881 /* give the running query some time to complete */
1882 return FALSE;
1884 rb_debug ("Database is read-only, delaying event processing\n");
1885 g_async_queue_push (db->priv->event_queue, event);
1886 goto next_event;
1889 switch (event->type) {
1890 case RHYTHMDB_EVENT_STAT:
1891 rb_debug ("processing RHYTHMDB_EVENT_STAT");
1892 rhythmdb_process_stat_event (db, event);
1893 break;
1894 case RHYTHMDB_EVENT_METADATA_LOAD:
1895 rb_debug ("processing RHYTHMDB_EVENT_METADATA_LOAD");
1896 free = rhythmdb_process_metadata_load (db, event);
1897 break;
1898 case RHYTHMDB_EVENT_ENTRY_SET:
1899 rb_debug ("processing RHYTHMDB_EVENT_ENTRY_SET");
1900 rhythmdb_process_queued_entry_set_event (db, event);
1901 break;
1902 case RHYTHMDB_EVENT_DB_LOAD:
1903 rb_debug ("processing RHYTHMDB_EVENT_DB_LOAD");
1904 g_signal_emit (G_OBJECT (db), rhythmdb_signals[LOAD_COMPLETE], 0);
1906 /* save the db every five minutes */
1907 if (db->priv->save_timeout_id > 0) {
1908 g_source_remove (db->priv->save_timeout_id);
1910 db->priv->save_timeout_id = g_timeout_add_full (G_PRIORITY_LOW,
1911 5 * 60 * 1000,
1912 (GSourceFunc) rhythmdb_idle_save,
1914 NULL);
1915 break;
1916 case RHYTHMDB_EVENT_THREAD_EXITED:
1917 rb_debug ("processing RHYTHMDB_EVENT_THREAD_EXITED");
1918 break;
1919 case RHYTHMDB_EVENT_DB_SAVED:
1920 rb_debug ("processing RHYTHMDB_EVENT_DB_SAVED");
1921 rhythmdb_read_leave (db);
1922 break;
1923 case RHYTHMDB_EVENT_QUERY_COMPLETE:
1924 rb_debug ("processing RHYTHMDB_EVENT_QUERY_COMPLETE");
1925 rhythmdb_read_leave (db);
1926 break;
1927 case RHYTHMDB_EVENT_FILE_CREATED_OR_MODIFIED:
1928 rb_debug ("processing RHYTHMDB_EVENT_FILE_CREATED_OR_MODIFIED");
1929 rhythmdb_process_file_created_or_modified (db, event);
1930 break;
1931 case RHYTHMDB_EVENT_FILE_DELETED:
1932 rb_debug ("processing RHYTHMDB_EVENT_FILE_DELETED");
1933 rhythmdb_process_file_deleted (db, event);
1934 break;
1936 if (free)
1937 rhythmdb_event_free (db, event);
1939 count++;
1940 next_event:
1941 if (timeout && (count % 8 == 0)) {
1942 GTimeVal now;
1943 g_get_current_time (&now);
1944 if (rb_compare_gtimeval (timeout,&now) < 0) {
1945 /* probably more work to do, so try to come back as soon as possible */
1946 return TRUE;
1951 /* queue is empty, so we can wait a while before checking it again */
1952 return FALSE;
1955 static gboolean
1956 rhythmdb_idle_poll_events (RhythmDB *db)
1958 gboolean poll_soon;
1959 GTimeVal timeout;
1961 g_get_current_time (&timeout);
1962 g_time_val_add (&timeout, G_USEC_PER_SEC*0.75);
1964 GDK_THREADS_ENTER ();
1966 poll_soon = rhythmdb_process_events (db, &timeout);
1968 if (poll_soon)
1969 db->priv->event_poll_id =
1970 g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) rhythmdb_idle_poll_events,
1971 db, NULL);
1972 else
1973 db->priv->event_poll_id =
1974 g_timeout_add (1000, (GSourceFunc) rhythmdb_idle_poll_events, db);
1976 GDK_THREADS_LEAVE ();
1978 return FALSE;
1981 #define READ_QUEUE_TIMEOUT G_USEC_PER_SEC / 10
1983 static gpointer
1984 read_queue (GAsyncQueue *queue, gboolean *cancel)
1986 GTimeVal timeout;
1987 gpointer ret;
1989 g_get_current_time (&timeout);
1990 g_time_val_add (&timeout, READ_QUEUE_TIMEOUT);
1992 if (G_UNLIKELY (*cancel))
1993 return NULL;
1994 while ((ret = g_async_queue_timed_pop (queue, &timeout)) == NULL) {
1995 if (G_UNLIKELY (*cancel))
1996 return NULL;
1997 g_get_current_time (&timeout);
1998 g_time_val_add (&timeout, G_USEC_PER_SEC);
2001 return ret;
2004 static void
2005 rhythmdb_execute_stat_info_cb (GnomeVFSAsyncHandle *handle,
2006 GList *results,
2007 /* GnomeVFSGetFileInfoResult* items */
2008 RhythmDBEvent *event)
2010 /* this is in the main thread, so we can't do any long operation here */
2011 GnomeVFSGetFileInfoResult *info_result = results->data;
2013 g_mutex_lock (event->db->priv->stat_mutex);
2014 event->db->priv->outstanding_stats = g_list_remove (event->db->priv->outstanding_stats, event);
2015 event->handle = NULL;
2016 g_mutex_unlock (event->db->priv->stat_mutex);
2018 if (info_result->result == GNOME_VFS_OK) {
2019 event->vfsinfo = gnome_vfs_file_info_dup (info_result->file_info);
2020 } else {
2021 event->error = make_access_failed_error (rb_refstring_get (event->real_uri),
2022 info_result->result);
2023 event->vfsinfo = NULL;
2025 g_async_queue_push (event->db->priv->event_queue, event);
2028 static void
2029 rhythmdb_execute_stat (RhythmDB *db,
2030 const char *uri,
2031 RhythmDBEvent *event)
2033 GnomeVFSURI *vfs_uri = gnome_vfs_uri_new (uri);
2035 GList *uri_list = g_list_append (NULL, vfs_uri);
2036 event->real_uri = rb_refstring_new (uri);
2038 g_mutex_lock (db->priv->stat_mutex);
2039 db->priv->outstanding_stats = g_list_prepend (db->priv->outstanding_stats, event);
2040 g_mutex_unlock (db->priv->stat_mutex);
2042 gnome_vfs_async_get_file_info (&event->handle, uri_list,
2043 GNOME_VFS_FILE_INFO_FOLLOW_LINKS,
2044 GNOME_VFS_PRIORITY_MIN,
2045 (GnomeVFSAsyncGetFileInfoCallback) rhythmdb_execute_stat_info_cb,
2046 event);
2047 gnome_vfs_uri_unref (vfs_uri);
2048 g_list_free (uri_list);
2051 void
2052 queue_stat_uri (const char *uri,
2053 RhythmDB *db,
2054 RhythmDBEntryType type)
2056 RhythmDBEvent *result;
2058 rb_debug ("queueing stat for \"%s\"", uri);
2059 g_assert (uri && *uri);
2061 result = g_new0 (RhythmDBEvent, 1);
2062 result->db = db;
2063 result->type = RHYTHMDB_EVENT_STAT;
2064 result->entry_type = type;
2067 * before the action thread is started, we queue up stat events,
2068 * as we're still creating and running queries, as well as loading
2069 * the database. when we start the action thread, we'll kick off
2070 * a gnome-vfs job to run all the stat events too.
2072 * when the action thread is already running, we can start the
2073 * async_get_file_info job directly.
2075 g_mutex_lock (db->priv->stat_mutex);
2076 if (db->priv->action_thread_running) {
2077 g_mutex_unlock (db->priv->stat_mutex);
2078 rhythmdb_execute_stat (db, uri, result);
2079 } else {
2080 GnomeVFSURI *vfs_uri;
2082 vfs_uri = gnome_vfs_uri_new (uri);
2084 /* construct a list of URIs and a hash table containing
2085 * stat events to fill in and post on the event queue.
2087 if (g_hash_table_lookup (db->priv->stat_events, vfs_uri)) {
2088 g_free (result);
2089 gnome_vfs_uri_unref (vfs_uri);
2090 } else {
2091 result->real_uri = rb_refstring_new (uri);
2092 g_hash_table_insert (db->priv->stat_events, vfs_uri, result);
2093 db->priv->stat_list = g_list_prepend (db->priv->stat_list, vfs_uri);
2096 g_mutex_unlock (db->priv->stat_mutex);
2100 static void
2101 queue_stat_uri_tad (const char *uri,
2102 RhythmDBAddThreadData *data)
2104 queue_stat_uri (uri, data->db, data->type);
2107 static gpointer
2108 add_thread_main (RhythmDBAddThreadData *data)
2110 RhythmDBEvent *result;
2112 rb_uri_handle_recursively (data->uri, (GFunc) queue_stat_uri_tad,
2113 &data->db->priv->exiting, data);
2115 rb_debug ("exiting");
2116 result = g_new0 (RhythmDBEvent, 1);
2117 result->db = data->db;
2118 result->type = RHYTHMDB_EVENT_THREAD_EXITED;
2119 g_async_queue_push (data->db->priv->event_queue, result);
2120 g_free (data->uri);
2121 g_free (data);
2122 return NULL;
2125 static void
2126 rhythmdb_execute_load (RhythmDB *db,
2127 const char *uri,
2128 RhythmDBEvent *event)
2130 GnomeVFSURI *vfs_uri = gnome_vfs_uri_new (uri);
2131 GnomeVFSResult vfsresult;
2133 event->real_uri = rb_refstring_new (rb_uri_resolve_symlink (uri));
2134 event->vfsinfo = gnome_vfs_file_info_new ();
2136 vfsresult = gnome_vfs_get_file_info (uri,
2137 event->vfsinfo,
2138 GNOME_VFS_FILE_INFO_FOLLOW_LINKS);
2139 if (vfsresult != GNOME_VFS_OK) {
2140 event->error = make_access_failed_error (uri, vfsresult);
2141 if (event->vfsinfo)
2142 gnome_vfs_file_info_unref (event->vfsinfo);
2143 event->vfsinfo = NULL;
2144 } else {
2145 if (event->type == RHYTHMDB_EVENT_METADATA_LOAD) {
2146 event->metadata = rb_metadata_new ();
2147 rb_metadata_load (event->metadata, rb_refstring_get (event->real_uri),
2148 &event->error);
2152 gnome_vfs_uri_unref (vfs_uri);
2153 g_async_queue_push (db->priv->event_queue, event);
2157 * rhythmdb_entry_get:
2158 * @entry: a #RhythmDBEntry.
2159 * @propid: the id of the property to get.
2160 * @val: return location for the property value.
2162 * Gets a property of an entry, storing it in the given #GValue.
2164 void
2165 rhythmdb_entry_get (RhythmDB *db,
2166 RhythmDBEntry *entry,
2167 RhythmDBPropType propid,
2168 GValue *val)
2170 g_return_if_fail (RHYTHMDB_IS (db));
2171 g_return_if_fail (entry != NULL);
2172 g_return_if_fail (entry->refcount > 0);
2174 rhythmdb_entry_sync_mirrored (entry, propid);
2176 g_assert (G_VALUE_TYPE (val) == rhythmdb_get_property_type (db, propid));
2177 switch (rhythmdb_property_type_map[propid]) {
2178 case G_TYPE_STRING:
2179 g_value_set_string (val, rhythmdb_entry_get_string (entry, propid));
2180 break;
2181 case G_TYPE_BOOLEAN:
2182 g_value_set_boolean (val, rhythmdb_entry_get_boolean (entry, propid));
2183 break;
2184 case G_TYPE_ULONG:
2185 g_value_set_ulong (val, rhythmdb_entry_get_ulong (entry, propid));
2186 break;
2187 case G_TYPE_UINT64:
2188 g_value_set_uint64 (val, rhythmdb_entry_get_uint64 (entry, propid));
2189 break;
2190 case G_TYPE_DOUBLE:
2191 g_value_set_double (val, rhythmdb_entry_get_double (entry, propid));
2192 break;
2193 case G_TYPE_POINTER:
2194 g_value_set_pointer (val, rhythmdb_entry_get_pointer (entry, propid));
2195 break;
2196 default:
2197 g_assert_not_reached ();
2198 break;
2202 static void
2203 entry_to_rb_metadata (RhythmDB *db,
2204 RhythmDBEntry *entry,
2205 RBMetaData *metadata)
2207 GValue val = {0, };
2208 int i;
2210 for (i = RHYTHMDB_PROP_TYPE; i != RHYTHMDB_NUM_PROPERTIES; i++) {
2211 RBMetaDataField field;
2213 if (metadata_field_from_prop (i, &field) == FALSE) {
2214 continue;
2217 g_value_init (&val, rhythmdb_property_type_map[i]);
2218 rhythmdb_entry_get (db, entry, i, &val);
2219 rb_metadata_set (metadata,
2220 field,
2221 &val);
2222 g_value_unset (&val);
2226 typedef struct
2228 RhythmDB *db;
2229 char *uri;
2230 GError *error;
2231 } RhythmDBSaveErrorData;
2233 static gboolean
2234 emit_save_error_idle (RhythmDBSaveErrorData *data)
2236 g_signal_emit (G_OBJECT (data->db), rhythmdb_signals[SAVE_ERROR], 0, data->uri, data->error);
2237 g_object_unref (G_OBJECT (data->db));
2238 g_free (data->uri);
2239 g_error_free (data->error);
2240 g_free (data);
2241 return FALSE;
2244 static gpointer
2245 action_thread_main (RhythmDB *db)
2247 RhythmDBEvent *result;
2249 while (TRUE) {
2250 RhythmDBAction *action;
2252 action = read_queue (db->priv->action_queue, &db->priv->exiting);
2254 if (action == NULL)
2255 break;
2257 switch (action->type) {
2258 case RHYTHMDB_ACTION_STAT:
2260 result = g_new0 (RhythmDBEvent, 1);
2261 result->db = db;
2262 result->type = RHYTHMDB_EVENT_STAT;
2263 result->entry_type = action->entry_type;
2265 rb_debug ("executing RHYTHMDB_ACTION_STAT for \"%s\"", rb_refstring_get (action->uri));
2267 rhythmdb_execute_stat (db, rb_refstring_get (action->uri), result);
2269 break;
2270 case RHYTHMDB_ACTION_LOAD:
2272 result = g_new0 (RhythmDBEvent, 1);
2273 result->db = db;
2274 result->type = RHYTHMDB_EVENT_METADATA_LOAD;
2275 result->entry_type = action->entry_type;
2277 rb_debug ("executing RHYTHMDB_ACTION_LOAD for \"%s\"", rb_refstring_get (action->uri));
2279 rhythmdb_execute_load (db, rb_refstring_get (action->uri), result);
2281 break;
2282 case RHYTHMDB_ACTION_SYNC:
2284 GError *error = NULL;
2285 RhythmDBEntry *entry;
2286 RhythmDBEntryType entry_type;
2288 if (db->priv->dry_run) {
2289 rb_debug ("dry run is enabled, not syncing metadata");
2290 break;
2293 entry = rhythmdb_entry_lookup_by_location_refstring (db, action->uri);
2294 if (!entry)
2295 break;
2297 entry_type = rhythmdb_entry_get_entry_type (entry);
2298 entry_type->sync_metadata (db, entry, &error, entry_type->sync_metadata_data);
2300 if (error != NULL) {
2301 RhythmDBSaveErrorData *data;
2303 data = g_new0 (RhythmDBSaveErrorData, 1);
2304 g_object_ref (db);
2305 data->db = db;
2306 data->uri = g_strdup (rb_refstring_get (action->uri));
2307 data->error = error;
2308 g_idle_add ((GSourceFunc)emit_save_error_idle, data);
2309 break;
2311 break;
2313 break;
2314 default:
2315 g_assert_not_reached ();
2316 break;
2318 rhythmdb_action_free (db, action);
2322 rb_debug ("exiting main thread");
2323 result = g_new0 (RhythmDBEvent, 1);
2324 result->db = db;
2325 result->type = RHYTHMDB_EVENT_THREAD_EXITED;
2326 g_async_queue_push (db->priv->event_queue, result);
2328 return NULL;
2332 * rhythmdb_add_uri:
2333 * @db: a #RhythmDB.
2334 * @uri: the URI to add an entry/entries for
2336 * Adds the file(s) pointed to by @uri to the database, as entries of type
2337 * RHYTHMDB_ENTRY_TYPE_SONG. If the URI is that of a file, they will be added.
2338 * If the URI is that of a directory, everything under it will be added recursively.
2340 void
2341 rhythmdb_add_uri (RhythmDB *db,
2342 const char *uri)
2344 rhythmdb_add_uri_with_type (db, uri, RHYTHMDB_ENTRY_TYPE_INVALID);
2347 void
2348 rhythmdb_add_uri_with_type (RhythmDB *db,
2349 const char *uri,
2350 RhythmDBEntryType type)
2352 char *realuri;
2353 char *canon_uri;
2355 canon_uri = rb_canonicalise_uri (uri);
2356 realuri = rb_uri_resolve_symlink (canon_uri);
2357 g_free (canon_uri);
2359 if (rb_uri_is_directory (realuri)) {
2360 RhythmDBAddThreadData *data = g_new0 (RhythmDBAddThreadData, 1);
2361 data->db = db;
2362 data->uri = g_strdup (realuri);
2363 data->type = type;
2365 rhythmdb_thread_create (db, db->priv->add_thread_pool, NULL, data);
2366 } else {
2367 queue_stat_uri (realuri, db, type);
2370 g_free (realuri);
2373 static gboolean
2374 rhythmdb_sync_library_idle (RhythmDB *db)
2376 rhythmdb_sync_library_location (db);
2377 g_object_unref (db);
2378 return FALSE;
2381 static gboolean
2382 rhythmdb_load_error_cb (GError *error)
2384 GDK_THREADS_ENTER ();
2385 rb_error_dialog (NULL,
2386 _("Could not load the music database"),
2387 error->message);
2388 g_error_free (error);
2390 GDK_THREADS_LEAVE ();
2391 return FALSE;
2394 static gpointer
2395 rhythmdb_load_thread_main (RhythmDB *db)
2397 RhythmDBEvent *result;
2398 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
2399 GError *error = NULL;
2401 rb_profile_start ("loading db");
2402 g_mutex_lock (db->priv->saving_mutex);
2403 if (klass->impl_load (db, &db->priv->exiting, &error) == FALSE) {
2404 rb_debug ("db load failed: disabling saving");
2405 db->priv->can_save = FALSE;
2407 if (error) {
2408 g_idle_add ((GSourceFunc) rhythmdb_load_error_cb, error);
2411 g_mutex_unlock (db->priv->saving_mutex);
2413 g_object_ref (db);
2414 g_timeout_add (10000, (GSourceFunc) rhythmdb_sync_library_idle, db);
2416 rb_debug ("queuing db load complete signal");
2417 result = g_new0 (RhythmDBEvent, 1);
2418 result->type = RHYTHMDB_EVENT_DB_LOAD;
2419 g_async_queue_push (db->priv->event_queue, result);
2421 rb_debug ("exiting");
2422 result = g_new0 (RhythmDBEvent, 1);
2423 result->type = RHYTHMDB_EVENT_THREAD_EXITED;
2424 g_async_queue_push (db->priv->event_queue, result);
2426 rb_profile_end ("loading db");
2427 return NULL;
2431 * rhythmdb_load:
2432 * @db: a #RhythmDB.
2434 * Load the database from disk.
2436 void
2437 rhythmdb_load (RhythmDB *db)
2439 rhythmdb_thread_create (db, NULL, (GThreadFunc) rhythmdb_load_thread_main, db);
2442 static gpointer
2443 rhythmdb_save_thread_main (RhythmDB *db)
2445 RhythmDBClass *klass;
2446 RhythmDBEvent *result;
2448 rb_debug ("entering save thread");
2450 g_mutex_lock (db->priv->saving_mutex);
2452 if (!db->priv->dirty && !db->priv->can_save) {
2453 rb_debug ("no save needed, ignoring");
2454 g_mutex_unlock (db->priv->saving_mutex);
2455 goto out;
2458 while (db->priv->saving)
2459 g_cond_wait (db->priv->saving_condition, db->priv->saving_mutex);
2461 db->priv->saving = TRUE;
2463 rb_debug ("saving rhythmdb");
2465 klass = RHYTHMDB_GET_CLASS (db);
2466 klass->impl_save (db);
2468 db->priv->saving = FALSE;
2469 db->priv->dirty = FALSE;
2471 g_mutex_unlock (db->priv->saving_mutex);
2473 g_cond_broadcast (db->priv->saving_condition);
2475 out:
2476 result = g_new0 (RhythmDBEvent, 1);
2477 result->db = db;
2478 result->type = RHYTHMDB_EVENT_DB_SAVED;
2479 g_async_queue_push (db->priv->event_queue, result);
2481 result = g_new0 (RhythmDBEvent, 1);
2482 result->db = db;
2483 result->type = RHYTHMDB_EVENT_THREAD_EXITED;
2484 g_async_queue_push (db->priv->event_queue, result);
2485 return NULL;
2489 * rhythmdb_save_async:
2490 * @db: a #RhythmDB.
2492 * Save the database to disk, asynchronously.
2494 void
2495 rhythmdb_save_async (RhythmDB *db)
2497 rb_debug ("saving the rhythmdb in the background");
2499 rhythmdb_read_enter (db);
2501 rhythmdb_thread_create (db, NULL, (GThreadFunc) rhythmdb_save_thread_main, db);
2505 * rhythmdb_save:
2506 * @db: a #RhythmDB.
2508 * Save the database to disk, not returning until it has been saved.
2510 void
2511 rhythmdb_save (RhythmDB *db)
2513 rb_debug("saving the rhythmdb and blocking");
2515 rhythmdb_save_async (db);
2517 g_mutex_lock (db->priv->saving_mutex);
2519 while (db->priv->saving)
2520 g_cond_wait (db->priv->saving_condition, db->priv->saving_mutex);
2522 g_mutex_unlock (db->priv->saving_mutex);
2526 * rhythmdb_entry_set:
2527 * @db:# a RhythmDB.
2528 * @entry: a #RhythmDBEntry.
2529 * @propid: the id of the property to set.
2530 * @value: the property value.
2532 * This function can be called by any code which wishes to change a
2533 * song property and send a notification. It may be called when the
2534 * database is read-only; in this case the change will be queued for
2535 * an unspecified time in the future. The implication of this is that
2536 * rhythmdb_entry_get() may not reflect the changes immediately. However,
2537 * if this property is exposed in the user interface, you should still
2538 * make the change in the widget. Then when the database returns to a
2539 * writable state, your change will take effect in the database too,
2540 * and a notification will be sent at that point.
2542 * Note that you must call rhythmdb_commit() at some point after invoking
2543 * this function, and that even after the commit, your change may not
2544 * have taken effect.
2546 void
2547 rhythmdb_entry_set (RhythmDB *db,
2548 RhythmDBEntry *entry,
2549 guint propid,
2550 const GValue *value)
2552 g_return_if_fail (RHYTHMDB_IS (db));
2553 g_return_if_fail (entry != NULL);
2555 if ((entry->flags & RHYTHMDB_ENTRY_INSERTED) != 0) {
2556 if (!rhythmdb_get_readonly (db) && rb_is_main_thread ()) {
2557 rhythmdb_entry_set_internal (db, entry, TRUE, propid, value);
2558 } else {
2559 RhythmDBEvent *result;
2561 result = g_new0 (RhythmDBEvent, 1);
2562 result->db = db;
2563 result->type = RHYTHMDB_EVENT_ENTRY_SET;
2565 rb_debug ("queuing RHYTHMDB_ACTION_ENTRY_SET");
2567 result->entry = rhythmdb_entry_ref (entry);
2568 result->change.prop = propid;
2569 result->signal_change = TRUE;
2570 g_value_init (&result->change.new, G_VALUE_TYPE (value));
2571 g_value_copy (value, &result->change.new);
2572 g_async_queue_push (db->priv->event_queue, result);
2574 } else {
2575 rhythmdb_entry_set_internal (db, entry, FALSE, propid, value);
2579 static void
2580 record_entry_change (RhythmDB *db,
2581 RhythmDBEntry *entry,
2582 guint propid,
2583 const GValue *value)
2585 RhythmDBEntryChange *changedata;
2586 GSList *changelist;
2588 changedata = g_new0 (RhythmDBEntryChange, 1);
2589 changedata->prop = propid;
2591 /* Copy a temporary gvalue, since _entry_get uses
2592 * _set_static_string to avoid memory allocations. */
2594 GValue tem = {0,};
2595 g_value_init (&tem, G_VALUE_TYPE (value));
2596 rhythmdb_entry_get (db, entry, propid, &tem);
2597 g_value_init (&changedata->old, G_VALUE_TYPE (value));
2598 g_value_copy (&tem, &changedata->old);
2599 g_value_unset (&tem);
2601 g_value_init (&changedata->new, G_VALUE_TYPE (value));
2602 g_value_copy (value, &changedata->new);
2604 g_mutex_lock (db->priv->change_mutex);
2605 /* ref the entry before adding to hash, it is unreffed when removed */
2606 rhythmdb_entry_ref (entry);
2607 changelist = g_hash_table_lookup (db->priv->changed_entries, entry);
2608 changelist = g_slist_append (changelist, changedata);
2609 g_hash_table_insert (db->priv->changed_entries, entry, changelist);
2610 g_mutex_unlock (db->priv->change_mutex);
2613 void
2614 rhythmdb_entry_set_internal (RhythmDB *db,
2615 RhythmDBEntry *entry,
2616 gboolean notify_if_inserted,
2617 guint propid,
2618 const GValue *value)
2620 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
2621 gboolean handled;
2622 RhythmDBPodcastFields *podcast = NULL;
2624 #ifndef G_DISABLE_ASSERT
2625 switch (G_VALUE_TYPE (value)) {
2626 case G_TYPE_STRING:
2627 /* the playback error is allowed to be NULL */
2628 if (propid != RHYTHMDB_PROP_PLAYBACK_ERROR || g_value_get_string (value))
2629 g_assert (g_utf8_validate (g_value_get_string (value), -1, NULL));
2630 break;
2631 case G_TYPE_BOOLEAN:
2632 case G_TYPE_ULONG:
2633 case G_TYPE_UINT64:
2634 case G_TYPE_DOUBLE:
2635 break;
2636 default:
2637 g_assert_not_reached ();
2638 break;
2640 #endif
2642 if ((entry->flags & RHYTHMDB_ENTRY_INSERTED) && notify_if_inserted) {
2643 record_entry_change (db, entry, propid, value);
2646 handled = klass->impl_entry_set (db, entry, propid, value);
2648 if (!handled) {
2649 if (entry->type == RHYTHMDB_ENTRY_TYPE_PODCAST_FEED ||
2650 entry->type == RHYTHMDB_ENTRY_TYPE_PODCAST_POST)
2651 podcast = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RhythmDBPodcastFields);
2653 switch (propid) {
2654 case RHYTHMDB_PROP_TYPE:
2655 case RHYTHMDB_PROP_ENTRY_ID:
2656 g_assert_not_reached ();
2657 break;
2658 case RHYTHMDB_PROP_TITLE:
2659 if (entry->title != NULL) {
2660 rb_refstring_unref (entry->title);
2662 entry->title = rb_refstring_new (g_value_get_string (value));
2663 break;
2664 case RHYTHMDB_PROP_ALBUM:
2665 if (entry->album != NULL) {
2666 rb_refstring_unref (entry->album);
2668 entry->album = rb_refstring_new (g_value_get_string (value));
2669 break;
2670 case RHYTHMDB_PROP_ARTIST:
2671 if (entry->artist != NULL) {
2672 rb_refstring_unref (entry->artist);
2674 entry->artist = rb_refstring_new (g_value_get_string (value));
2675 break;
2676 case RHYTHMDB_PROP_GENRE:
2677 if (entry->genre != NULL) {
2678 rb_refstring_unref (entry->genre);
2680 entry->genre = rb_refstring_new (g_value_get_string (value));
2681 break;
2682 case RHYTHMDB_PROP_TRACK_NUMBER:
2683 entry->tracknum = g_value_get_ulong (value);
2684 break;
2685 case RHYTHMDB_PROP_DISC_NUMBER:
2686 entry->discnum = g_value_get_ulong (value);
2687 break;
2688 case RHYTHMDB_PROP_DURATION:
2689 entry->duration = g_value_get_ulong (value);
2690 break;
2691 case RHYTHMDB_PROP_BITRATE:
2692 entry->bitrate = g_value_get_ulong (value);
2693 break;
2694 case RHYTHMDB_PROP_DATE:
2696 gulong julian;
2697 julian = g_value_get_ulong (value);
2698 if (julian > 0)
2699 g_date_set_julian (&entry->date, julian);
2700 else
2701 g_date_clear (&entry->date, 1);
2702 break;
2704 case RHYTHMDB_PROP_TRACK_GAIN:
2705 entry->track_gain = g_value_get_double (value);
2706 break;
2707 case RHYTHMDB_PROP_TRACK_PEAK:
2708 entry->track_peak = g_value_get_double (value);
2709 break;
2710 case RHYTHMDB_PROP_ALBUM_GAIN:
2711 entry->album_gain = g_value_get_double (value);
2712 break;
2713 case RHYTHMDB_PROP_ALBUM_PEAK:
2714 entry->album_peak = g_value_get_double (value);
2715 break;
2716 case RHYTHMDB_PROP_LOCATION:
2717 rb_refstring_unref (entry->location);
2718 entry->location = rb_refstring_new (g_value_get_string (value));
2719 break;
2720 case RHYTHMDB_PROP_PLAYBACK_ERROR:
2721 rb_refstring_unref (entry->playback_error);
2722 if (g_value_get_string (value))
2723 entry->playback_error = rb_refstring_new (g_value_get_string (value));
2724 else
2725 entry->playback_error = NULL;
2726 break;
2727 case RHYTHMDB_PROP_MOUNTPOINT:
2728 if (entry->mountpoint != NULL) {
2729 rb_refstring_unref (entry->mountpoint);
2731 entry->mountpoint = rb_refstring_new (g_value_get_string (value));
2732 break;
2733 case RHYTHMDB_PROP_FILE_SIZE:
2734 entry->file_size = g_value_get_uint64 (value);
2735 break;
2736 case RHYTHMDB_PROP_MIMETYPE:
2737 if (entry->mimetype != NULL) {
2738 rb_refstring_unref (entry->mimetype);
2740 entry->mimetype = rb_refstring_new (g_value_get_string (value));
2741 break;
2742 case RHYTHMDB_PROP_MTIME:
2743 entry->mtime = g_value_get_ulong (value);
2744 break;
2745 case RHYTHMDB_PROP_FIRST_SEEN:
2746 entry->first_seen = g_value_get_ulong (value);
2747 entry->flags |= RHYTHMDB_ENTRY_FIRST_SEEN_DIRTY;
2748 break;
2749 case RHYTHMDB_PROP_LAST_SEEN:
2750 entry->last_seen = g_value_get_ulong (value);
2751 entry->flags |= RHYTHMDB_ENTRY_LAST_SEEN_DIRTY;
2752 break;
2753 case RHYTHMDB_PROP_RATING:
2754 entry->rating = g_value_get_double (value);
2755 break;
2756 case RHYTHMDB_PROP_PLAY_COUNT:
2757 entry->play_count = g_value_get_ulong (value);
2758 break;
2759 case RHYTHMDB_PROP_LAST_PLAYED:
2760 entry->last_played = g_value_get_ulong (value);
2761 entry->flags |= RHYTHMDB_ENTRY_LAST_PLAYED_DIRTY;
2762 break;
2763 case RHYTHMDB_PROP_MUSICBRAINZ_TRACKID:
2764 rb_refstring_unref (entry->musicbrainz_trackid);
2765 entry->musicbrainz_trackid = rb_refstring_new (g_value_get_string (value));
2766 break;
2767 case RHYTHMDB_PROP_HIDDEN:
2768 if (g_value_get_boolean (value)) {
2769 entry->flags |= RHYTHMDB_ENTRY_HIDDEN;
2770 } else {
2771 entry->flags &= ~RHYTHMDB_ENTRY_HIDDEN;
2773 entry->flags |= RHYTHMDB_ENTRY_LAST_SEEN_DIRTY;
2774 break;
2775 case RHYTHMDB_PROP_STATUS:
2776 g_assert (podcast);
2777 podcast->status = g_value_get_ulong (value);
2778 break;
2779 case RHYTHMDB_PROP_DESCRIPTION:
2780 g_assert (podcast);
2781 rb_refstring_unref (podcast->description);
2782 podcast->description = rb_refstring_new (g_value_get_string (value));
2783 break;
2784 case RHYTHMDB_PROP_SUBTITLE:
2785 g_assert (podcast);
2786 rb_refstring_unref (podcast->subtitle);
2787 podcast->subtitle = rb_refstring_new (g_value_get_string (value));
2788 break;
2789 case RHYTHMDB_PROP_SUMMARY:
2790 g_assert (podcast);
2791 rb_refstring_unref (podcast->summary);
2792 podcast->summary = rb_refstring_new (g_value_get_string (value));
2793 break;
2794 case RHYTHMDB_PROP_LANG:
2795 g_assert (podcast);
2796 if (podcast->lang != NULL) {
2797 rb_refstring_unref (podcast->lang);
2799 podcast->lang = rb_refstring_new (g_value_get_string (value));
2800 break;
2801 case RHYTHMDB_PROP_COPYRIGHT:
2802 g_assert (podcast);
2803 if (podcast->copyright != NULL) {
2804 rb_refstring_unref (podcast->copyright);
2806 podcast->copyright = rb_refstring_new (g_value_get_string (value));
2807 break;
2808 case RHYTHMDB_PROP_IMAGE:
2809 g_assert (podcast);
2810 if (podcast->image != NULL) {
2811 rb_refstring_unref (podcast->image);
2813 podcast->image = rb_refstring_new (g_value_get_string (value));
2814 break;
2815 case RHYTHMDB_PROP_POST_TIME:
2816 g_assert (podcast);
2817 podcast->post_time = g_value_get_ulong (value);
2818 break;
2819 case RHYTHMDB_NUM_PROPERTIES:
2820 g_assert_not_reached ();
2821 break;
2825 /* set the dirty state */
2826 db->priv->dirty = TRUE;
2830 * rhythmdb_entry_sync_mirrored:
2831 * @db: a #RhythmDB.
2832 * @type: a #RhythmDBEntry.
2833 * @propid: the property to sync the mirrored version of.
2835 * Synchronise "mirrored" properties, such as the string version of the last-played
2836 * time. This should be called when a property is directly modified, passing the
2837 * original property.
2839 * This should only be used by RhythmDB itself, or a backend (such as rhythmdb-tree).
2842 static void
2843 rhythmdb_entry_sync_mirrored (RhythmDBEntry *entry,
2844 guint propid)
2846 static const char *format;
2847 static const char *never;
2848 char *val;
2850 /* PERFORMANCE: doing these lookups every call creates a noticable slowdown */
2851 if (format == NULL)
2852 format = _("%Y-%m-%d %H:%M");
2853 if (never == NULL)
2854 never = _("Never");
2856 switch (propid) {
2857 case RHYTHMDB_PROP_LAST_PLAYED_STR:
2859 RBRefString *old, *new;
2861 if (!(entry->flags & RHYTHMDB_ENTRY_LAST_PLAYED_DIRTY))
2862 break;
2864 old = g_atomic_pointer_get (&entry->last_played_str);
2865 if (entry->last_played == 0) {
2866 new = rb_refstring_new (never);
2867 } else {
2868 val = eel_strdup_strftime (format, localtime ((glong*)&entry->last_played));
2869 new = rb_refstring_new (val);
2870 g_free (val);
2873 if (g_atomic_pointer_compare_and_exchange (&entry->last_played_str, old, new)) {
2874 if (old != NULL) {
2875 rb_refstring_unref (old);
2877 } else {
2878 rb_refstring_unref (new);
2881 break;
2883 case RHYTHMDB_PROP_FIRST_SEEN_STR:
2885 RBRefString *old, *new;
2887 if (!(entry->flags & RHYTHMDB_ENTRY_FIRST_SEEN_DIRTY))
2888 break;
2890 old = g_atomic_pointer_get (&entry->first_seen_str);
2891 val = eel_strdup_strftime (format, localtime ((glong*)&entry->first_seen));
2892 new = rb_refstring_new (val);
2893 g_free (val);
2895 if (g_atomic_pointer_compare_and_exchange (&entry->first_seen_str, old, new)) {
2896 if (old != NULL) {
2897 rb_refstring_unref (old);
2899 } else {
2900 rb_refstring_unref (new);
2903 break;
2905 case RHYTHMDB_PROP_LAST_SEEN_STR:
2907 RBRefString *old, *new;
2909 if (!(entry->flags & RHYTHMDB_ENTRY_LAST_SEEN_DIRTY))
2910 break;
2912 old = g_atomic_pointer_get (&entry->last_seen_str);
2913 /* only store last seen time as a string for hidden entries */
2914 if (entry->flags & RHYTHMDB_ENTRY_HIDDEN) {
2915 val = eel_strdup_strftime (format, localtime ((glong*)&entry->last_seen));
2916 new = rb_refstring_new (val);
2917 g_free (val);
2918 } else {
2919 new = NULL;
2922 if (g_atomic_pointer_compare_and_exchange (&entry->first_seen_str, old, new)) {
2923 if (old != NULL) {
2924 rb_refstring_unref (old);
2926 } else {
2927 rb_refstring_unref (new);
2930 break;
2932 default:
2933 break;
2938 * rhythmdb_entry_delete:
2939 * @db: a #RhythmDB.
2940 * @entry: a #RhythmDBEntry.
2942 * Delete entry @entry from the database, sending notification of it's deletion.
2943 * This is usually used by sources where entries can disappear randomly, such
2944 * as a network source.
2946 void
2947 rhythmdb_entry_delete (RhythmDB *db,
2948 RhythmDBEntry *entry)
2950 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
2952 g_return_if_fail (RHYTHMDB_IS (db));
2953 g_return_if_fail (entry != NULL);
2955 klass->impl_entry_delete (db, entry);
2957 /* ref the entry before adding to hash, it is unreffed when removed */
2958 rhythmdb_entry_ref (entry);
2959 g_mutex_lock (db->priv->change_mutex);
2960 g_hash_table_insert (db->priv->deleted_entries, entry, g_thread_self ());
2961 g_mutex_unlock (db->priv->change_mutex);
2963 /* deleting an entry makes the db dirty */
2964 db->priv->dirty = TRUE;
2967 static gint
2968 rhythmdb_entry_move_to_trash_cb (GnomeVFSXferProgressInfo *info,
2969 gpointer data)
2971 /* Abort immediately if anything happens */
2972 if (info->status == GNOME_VFS_XFER_PROGRESS_STATUS_VFSERROR)
2973 return GNOME_VFS_XFER_ERROR_ACTION_ABORT;
2974 /* Don't overwrite files */
2975 if (info->status == GNOME_VFS_XFER_PROGRESS_STATUS_OVERWRITE)
2976 return 0;
2977 return TRUE;
2980 static void
2981 rhythmdb_entry_move_to_trash_set_error (RhythmDB *db,
2982 RhythmDBEntry *entry,
2983 GnomeVFSResult res)
2985 GValue value = { 0, };
2987 if (res == -1)
2988 res = GNOME_VFS_ERROR_INTERNAL;
2990 g_value_init (&value, G_TYPE_STRING);
2991 g_value_set_string (&value, gnome_vfs_result_to_string (res));
2992 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &value);
2993 g_value_unset (&value);
2995 rb_debug ("Deleting %s failed: %s", rb_refstring_get (entry->location),
2996 gnome_vfs_result_to_string (res));
2999 void
3000 rhythmdb_entry_move_to_trash (RhythmDB *db,
3001 RhythmDBEntry *entry)
3003 GnomeVFSResult res;
3004 GnomeVFSURI *uri, *trash, *dest;
3005 char *shortname;
3007 uri = gnome_vfs_uri_new (rb_refstring_get (entry->location));
3008 if (uri == NULL) {
3009 rhythmdb_entry_move_to_trash_set_error (db, entry, -1);
3010 return;
3013 res = gnome_vfs_find_directory (uri,
3014 GNOME_VFS_DIRECTORY_KIND_TRASH,
3015 &trash,
3016 TRUE, TRUE,
3018 if (res != GNOME_VFS_OK || trash == NULL) {
3019 /* If the file doesn't exist, or trash isn't support,
3020 * remove it from the db */
3021 if (res == GNOME_VFS_ERROR_NOT_FOUND ||
3022 res == GNOME_VFS_ERROR_NOT_SUPPORTED) {
3023 rhythmdb_entry_set_visibility (db, entry, FALSE);
3024 } else {
3025 rhythmdb_entry_move_to_trash_set_error (db, entry, -1);
3028 gnome_vfs_uri_unref (uri);
3029 return;
3032 /* Is the file already in the Trash? If so it should be hidden */
3033 if (gnome_vfs_uri_is_parent (trash, uri, TRUE)) {
3034 GValue value = { 0, };
3035 g_value_init (&value, G_TYPE_BOOLEAN);
3036 g_value_set_boolean (&value, TRUE);
3037 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_HIDDEN, &value);
3038 rhythmdb_commit (db);
3040 gnome_vfs_uri_unref (trash);
3041 gnome_vfs_uri_unref (uri);
3042 return;
3045 shortname = gnome_vfs_uri_extract_short_name (uri);
3046 if (shortname == NULL) {
3047 rhythmdb_entry_move_to_trash_set_error (db, entry, -1);
3048 rhythmdb_commit (db);
3049 gnome_vfs_uri_unref (uri);
3050 gnome_vfs_uri_unref (trash);
3051 return;
3054 /* Compute the destination URI */
3055 dest = gnome_vfs_uri_append_path (trash, shortname);
3056 gnome_vfs_uri_unref (trash);
3057 g_free (shortname);
3058 if (dest == NULL) {
3059 rhythmdb_entry_move_to_trash_set_error (db, entry, -1);
3060 rhythmdb_commit (db);
3061 gnome_vfs_uri_unref (uri);
3062 return;
3065 /* RB can't tell that a file's moved, so no unique names */
3066 res = gnome_vfs_xfer_uri (uri, dest,
3067 GNOME_VFS_XFER_REMOVESOURCE,
3068 GNOME_VFS_XFER_ERROR_MODE_ABORT,
3069 GNOME_VFS_XFER_OVERWRITE_MODE_SKIP,
3070 rhythmdb_entry_move_to_trash_cb,
3071 entry);
3073 if (res == GNOME_VFS_OK) {
3074 rhythmdb_entry_set_visibility (db, entry, FALSE);
3075 } else {
3076 rhythmdb_entry_move_to_trash_set_error (db, entry, res);
3078 rhythmdb_commit (db);
3080 gnome_vfs_uri_unref (dest);
3081 gnome_vfs_uri_unref (uri);
3085 * rhythmdb_entry_delete_by_type:
3086 * @db: a #RhythmDB.
3087 * @type: type of entried to delete.
3089 * Delete all entries from the database of the given type.
3090 * This is usually used by non-permanent sources when they disappear, such as
3091 * removable media being removed, or a network share becoming unavailable.
3093 void
3094 rhythmdb_entry_delete_by_type (RhythmDB *db,
3095 RhythmDBEntryType type)
3097 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
3099 if (klass->impl_entry_delete_by_type) {
3100 klass->impl_entry_delete_by_type (db, type);
3101 } else {
3102 g_warning ("delete_by_type not implemented");
3106 const xmlChar *
3107 rhythmdb_nice_elt_name_from_propid (RhythmDB *db,
3108 RhythmDBPropType propid)
3110 return db->priv->column_xml_names[propid];
3114 rhythmdb_propid_from_nice_elt_name (RhythmDB *db,
3115 const xmlChar *name)
3117 gpointer ret, orig;
3118 if (g_hash_table_lookup_extended (db->priv->propname_map, name,
3119 &orig, &ret)) {
3120 return GPOINTER_TO_INT (ret);
3122 return -1;
3126 * rhythmdb_entry_lookup_by_location:
3127 * @db: a #RhythmDB.
3128 * @uri: the URI of the entry to lookup.
3130 * Looks up the entry with location @uri.
3132 * Returns: the entry with location @uri, or NULL if no such entry exists.
3134 RhythmDBEntry *
3135 rhythmdb_entry_lookup_by_location (RhythmDB *db,
3136 const char *uri)
3138 RBRefString *rs;
3140 rs = rb_refstring_find (uri);
3141 if (rs != NULL) {
3142 return rhythmdb_entry_lookup_by_location_refstring (db, rs);
3143 } else {
3144 return NULL;
3148 RhythmDBEntry *
3149 rhythmdb_entry_lookup_by_location_refstring (RhythmDB *db,
3150 RBRefString *uri)
3152 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
3154 return klass->impl_lookup_by_location (db, uri);
3158 *rhythmdb_entry_foreach:
3159 * @db: a #RhythmDB.
3160 * @func: the function to call with each entry.
3161 * @data: user data to pass to the function.
3163 * Calls the given function for each of the entries in the database.
3165 void
3166 rhythmdb_entry_foreach (RhythmDB *db,
3167 GFunc func,
3168 gpointer data)
3170 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
3172 klass->impl_entry_foreach (db, func, data);
3176 * rhythmdb_evaluate_query:
3177 * @db: a #RhythmDB.
3178 * @query: a query.
3179 * @entry a @RhythmDBEntry.
3181 * Evaluates the given entry against the given query.
3183 * Returns: whether the given entry matches the criteria of the given query.
3185 gboolean
3186 rhythmdb_evaluate_query (RhythmDB *db,
3187 GPtrArray *query,
3188 RhythmDBEntry *entry)
3190 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
3192 return klass->impl_evaluate_query (db, query, entry);
3195 static void
3196 rhythmdb_query_internal (RhythmDBQueryThreadData *data)
3198 RhythmDBEvent *result;
3199 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (data->db);
3201 rhythmdb_query_preprocess (data->db, data->query);
3203 rb_debug ("doing query");
3205 klass->impl_do_full_query (data->db, data->query,
3206 data->results,
3207 &data->cancel);
3209 rb_debug ("completed");
3210 rhythmdb_query_results_query_complete (data->results);
3212 result = g_new0 (RhythmDBEvent, 1);
3213 result->db = data->db;
3214 result->type = RHYTHMDB_EVENT_QUERY_COMPLETE;
3215 result->results = data->results;
3216 g_async_queue_push (data->db->priv->event_queue, result);
3218 rhythmdb_query_free (data->query);
3221 static gpointer
3222 query_thread_main (RhythmDBQueryThreadData *data)
3224 RhythmDBEvent *result;
3226 rb_debug ("entering query thread");
3228 rhythmdb_query_internal (data);
3230 result = g_new0 (RhythmDBEvent, 1);
3231 result->db = data->db;
3232 result->type = RHYTHMDB_EVENT_THREAD_EXITED;
3233 g_async_queue_push (data->db->priv->event_queue, result);
3234 g_free (data);
3235 return NULL;
3238 void
3239 rhythmdb_do_full_query_async_parsed (RhythmDB *db,
3240 RhythmDBQueryResults *results,
3241 GPtrArray *query)
3243 RhythmDBQueryThreadData *data;
3245 data = g_new0 (RhythmDBQueryThreadData, 1);
3246 data->db = db;
3247 data->query = rhythmdb_query_copy (query);
3248 data->results = results;
3249 data->cancel = FALSE;
3251 rhythmdb_read_enter (db);
3253 rhythmdb_query_results_set_query (results, query);
3255 g_object_ref (results);
3256 g_object_ref (db);
3257 g_atomic_int_inc (&db->priv->outstanding_threads);
3258 g_async_queue_ref (db->priv->action_queue);
3259 g_async_queue_ref (db->priv->event_queue);
3260 g_thread_pool_push (db->priv->query_thread_pool, data, NULL);
3263 void
3264 rhythmdb_do_full_query_async (RhythmDB *db,
3265 RhythmDBQueryResults *results,
3266 ...)
3268 GPtrArray *query;
3269 va_list args;
3271 va_start (args, results);
3273 query = rhythmdb_query_parse_valist (db, args);
3275 rhythmdb_do_full_query_async_parsed (db, results, query);
3277 rhythmdb_query_free (query);
3279 va_end (args);
3282 static void
3283 rhythmdb_do_full_query_internal (RhythmDB *db,
3284 RhythmDBQueryResults *results,
3285 GPtrArray *query)
3287 RhythmDBQueryThreadData *data;
3289 data = g_new0 (RhythmDBQueryThreadData, 1);
3290 data->db = db;
3291 data->query = rhythmdb_query_copy (query);
3292 data->results = results;
3293 data->cancel = FALSE;
3295 rhythmdb_read_enter (db);
3297 rhythmdb_query_results_set_query (results, query);
3298 g_object_ref (results);
3300 rhythmdb_query_internal (data);
3301 g_free (data);
3304 void
3305 rhythmdb_do_full_query_parsed (RhythmDB *db,
3306 RhythmDBQueryResults *results,
3307 GPtrArray *query)
3309 rhythmdb_do_full_query_internal (db, results, query);
3312 void
3313 rhythmdb_do_full_query (RhythmDB *db,
3314 RhythmDBQueryResults *results,
3315 ...)
3317 GPtrArray *query;
3318 va_list args;
3320 va_start (args, results);
3322 query = rhythmdb_query_parse_valist (db, args);
3324 rhythmdb_do_full_query_internal (db, results, query);
3326 rhythmdb_query_free (query);
3328 va_end (args);
3331 /* This should really be standard. */
3332 #define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
3334 GType
3335 rhythmdb_query_type_get_type (void)
3337 static GType etype = 0;
3339 if (etype == 0)
3341 static const GEnumValue values[] =
3344 ENUM_ENTRY (RHYTHMDB_QUERY_END, "Query end marker"),
3345 ENUM_ENTRY (RHYTHMDB_QUERY_DISJUNCTION, "Disjunctive marker"),
3346 ENUM_ENTRY (RHYTHMDB_QUERY_SUBQUERY, "Subquery"),
3347 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_EQUALS, "Property equivalence"),
3348 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_LIKE, "Fuzzy property matching"),
3349 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_NOT_LIKE, "Inverted fuzzy property matching"),
3350 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_PREFIX, "Starts with"),
3351 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_SUFFIX, "Ends with"),
3352 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_GREATER, "True if property1 >= property2"),
3353 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_LESS, "True if property1 <= property2"),
3354 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_CURRENT_TIME_WITHIN, "True if property1 is within property2 of the current time"),
3355 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_CURRENT_TIME_NOT_WITHIN, "True if property1 is not within property2 of the current time"),
3356 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_YEAR_EQUALS, "Year equivalence: true if date within year"),
3357 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_YEAR_GREATER, "True if date greater than year"),
3358 ENUM_ENTRY (RHYTHMDB_QUERY_PROP_YEAR_LESS, "True if date less than year"),
3359 { 0, 0, 0 }
3362 etype = g_enum_register_static ("RhythmDBQueryType", values);
3365 return etype;
3368 GType
3369 rhythmdb_prop_type_get_type (void)
3371 static GType etype = 0;
3373 if (etype == 0)
3375 static const GEnumValue values[] =
3377 /* We reuse the description to store extra data about
3378 * a property. The first part is just a generic
3379 * human-readable description. Next, there is
3380 * a string describing the GType of the property, in
3381 * parenthesis.
3382 * Finally, there is the XML element name in brackets.
3384 ENUM_ENTRY (RHYTHMDB_PROP_TYPE, "Type of entry (gpointer) [type]"),
3385 ENUM_ENTRY (RHYTHMDB_PROP_ENTRY_ID, "Numeric ID (guint) [entry-id]"),
3386 ENUM_ENTRY (RHYTHMDB_PROP_TITLE, "Title (gchararray) [title]"),
3387 ENUM_ENTRY (RHYTHMDB_PROP_GENRE, "Genre (gchararray) [genre]"),
3388 ENUM_ENTRY (RHYTHMDB_PROP_ARTIST, "Artist (gchararray) [artist]"),
3389 ENUM_ENTRY (RHYTHMDB_PROP_ALBUM, "Album (gchararray) [album]"),
3390 ENUM_ENTRY (RHYTHMDB_PROP_TRACK_NUMBER, "Track Number (gulong) [track-number]"),
3391 ENUM_ENTRY (RHYTHMDB_PROP_DISC_NUMBER, "Disc Number (gulong) [disc-number]"),
3392 ENUM_ENTRY (RHYTHMDB_PROP_MUSICBRAINZ_TRACKID, "Musicbrainz Track ID (gchararray) [mb-trackid]"),
3394 ENUM_ENTRY (RHYTHMDB_PROP_DURATION, "Duration (gulong) [duration]"),
3395 ENUM_ENTRY (RHYTHMDB_PROP_FILE_SIZE, "File Size (guint64) [file-size]"),
3396 ENUM_ENTRY (RHYTHMDB_PROP_LOCATION, "Location (gchararray) [location]"),
3397 ENUM_ENTRY (RHYTHMDB_PROP_MOUNTPOINT, "Mount point it's located in (gchararray) [mountpoint]"),
3398 ENUM_ENTRY (RHYTHMDB_PROP_MTIME, "Modification time (gulong) [mtime]"),
3399 ENUM_ENTRY (RHYTHMDB_PROP_FIRST_SEEN, "Time the song was added to the library (gulong) [first-seen]"),
3400 ENUM_ENTRY (RHYTHMDB_PROP_LAST_SEEN, "Last time the song was available (gulong) [last-seen]"),
3401 ENUM_ENTRY (RHYTHMDB_PROP_RATING, "Rating (gdouble) [rating]"),
3402 ENUM_ENTRY (RHYTHMDB_PROP_PLAY_COUNT, "Play Count (gulong) [play-count]"),
3403 ENUM_ENTRY (RHYTHMDB_PROP_LAST_PLAYED, "Last Played (gulong) [last-played]"),
3404 ENUM_ENTRY (RHYTHMDB_PROP_BITRATE, "Bitrate (gulong) [bitrate]"),
3405 ENUM_ENTRY (RHYTHMDB_PROP_DATE, "Date of release (gulong) [date]"),
3406 ENUM_ENTRY (RHYTHMDB_PROP_TRACK_GAIN, "Replaygain track gain (gdouble) [replaygain-track-gain]"),
3407 ENUM_ENTRY (RHYTHMDB_PROP_TRACK_PEAK, "Replaygain track peak (gdouble) [replaygain-track-peak]"),
3408 ENUM_ENTRY (RHYTHMDB_PROP_ALBUM_GAIN, "Replaygain album pain (gdouble) [replaygain-album-gain]"),
3409 ENUM_ENTRY (RHYTHMDB_PROP_ALBUM_PEAK, "Replaygain album peak (gdouble) [replaygain-album-peak]"),
3410 ENUM_ENTRY (RHYTHMDB_PROP_MIMETYPE, "Mime Type (gchararray) [mimetype]"),
3411 ENUM_ENTRY (RHYTHMDB_PROP_TITLE_SORT_KEY, "Title sort key (gchararray) [title-sort-key]"),
3412 ENUM_ENTRY (RHYTHMDB_PROP_GENRE_SORT_KEY, "Genre sort key (gchararray) [genre-sort-key]"),
3413 ENUM_ENTRY (RHYTHMDB_PROP_ARTIST_SORT_KEY, "Artist sort key (gchararray) [artist-sort-key]"),
3414 ENUM_ENTRY (RHYTHMDB_PROP_ALBUM_SORT_KEY, "Album sort key (gchararray) [album-sort-key]"),
3416 ENUM_ENTRY (RHYTHMDB_PROP_TITLE_FOLDED, "Title folded (gchararray) [title-folded]"),
3417 ENUM_ENTRY (RHYTHMDB_PROP_GENRE_FOLDED, "Genre folded (gchararray) [genre-folded]"),
3418 ENUM_ENTRY (RHYTHMDB_PROP_ARTIST_FOLDED, "Artist folded (gchararray) [artist-folded]"),
3419 ENUM_ENTRY (RHYTHMDB_PROP_ALBUM_FOLDED, "Album folded (gchararray) [album-folded]"),
3420 ENUM_ENTRY (RHYTHMDB_PROP_LAST_PLAYED_STR, "Last Played (gchararray) [last-played-str]"),
3421 ENUM_ENTRY (RHYTHMDB_PROP_PLAYBACK_ERROR, "Playback error string (gchararray) [playback-error]"),
3422 ENUM_ENTRY (RHYTHMDB_PROP_HIDDEN, "Hidden (gboolean) [hidden]"),
3423 ENUM_ENTRY (RHYTHMDB_PROP_FIRST_SEEN_STR, "Time Added to Library (gchararray) [first-seen-str]"),
3424 ENUM_ENTRY (RHYTHMDB_PROP_LAST_SEEN_STR, "Last time the song was available (gchararray) [last-seen-str]"),
3425 ENUM_ENTRY (RHYTHMDB_PROP_SEARCH_MATCH, "Search matching key (gchararray) [search-match]"),
3426 ENUM_ENTRY (RHYTHMDB_PROP_YEAR, "Year of date (gulong) [year]"),
3428 ENUM_ENTRY (RHYTHMDB_PROP_STATUS, "Status of file (gulong) [status]"),
3429 ENUM_ENTRY (RHYTHMDB_PROP_DESCRIPTION, "Podcast description(gchararray) [description]"),
3430 ENUM_ENTRY (RHYTHMDB_PROP_SUBTITLE, "Podcast subtitle (gchararray) [subtitle]"),
3431 ENUM_ENTRY (RHYTHMDB_PROP_SUMMARY, "Podcast summary (gchararray) [summary]"),
3432 ENUM_ENTRY (RHYTHMDB_PROP_LANG, "Podcast language (gchararray) [lang]"),
3433 ENUM_ENTRY (RHYTHMDB_PROP_COPYRIGHT, "Podcast copyright (gchararray) [copyright]"),
3434 ENUM_ENTRY (RHYTHMDB_PROP_IMAGE, "Podcast image(gchararray) [image]"),
3435 ENUM_ENTRY (RHYTHMDB_PROP_POST_TIME, "Podcast time of post (gulong) [post-time]"),
3436 { 0, 0, 0 }
3438 g_assert ((sizeof (values) / sizeof (values[0]) - 1) == RHYTHMDB_NUM_PROPERTIES);
3439 etype = g_enum_register_static ("RhythmDBPropType", values);
3442 return etype;
3445 void
3446 rhythmdb_emit_entry_deleted (RhythmDB *db,
3447 RhythmDBEntry *entry)
3449 g_signal_emit (G_OBJECT (db), rhythmdb_signals[ENTRY_DELETED], 0, entry);
3452 static gboolean
3453 rhythmdb_entry_extra_metadata_accumulator (GSignalInvocationHint *ihint,
3454 GValue *return_accu,
3455 const GValue *handler_return,
3456 gpointer data)
3458 if (handler_return == NULL)
3459 return TRUE;
3461 g_value_copy (handler_return, return_accu);
3462 return (g_value_get_boxed (return_accu) == NULL);
3466 * rhythmdb_entry_request_extra_metadata:
3467 * @db: a #RhythmDB
3468 * @entry: a #RhythmDBEntry
3469 * @property_name: the metadata predicate
3471 * Emits a request for extra metadata for the @entry.
3472 * The @property_name argument is emitted as the ::detail part of the
3473 * "entry_extra_metadata_request" signal. It should be a namespaced RDF
3474 * predicate e.g. from Dublin Core, MusicBrainz, or internal to Rhythmbox
3475 * (namespace "rb:"). Suitable predicates would be those that are expensive to
3476 * acquire or only apply to a limited range of entries.
3477 * Handlers capable of providing a particular predicate may ensure they only
3478 * see appropriate requests by supplying an appropriate ::detail part when
3479 * connecting to the signal. Upon a handler returning a non-%NULL value,
3480 * emission will be stopped and the value returned to the caller; if no
3481 * handlers return a non-%NULL value, the caller will receive %NULL. Priority
3482 * is determined by signal connection order, with %G_CONNECT_AFTER providing a
3483 * second, lower rank of priority.
3484 * A handler returning a value should do so in a #GValue allocated on the heap;
3485 * the accumulator will take ownership. The caller should unset and free the
3486 * #GValue if non-%NULL when finished with it.
3488 * Returns: an allocated, initialised, set #GValue, or NULL
3490 GValue *
3491 rhythmdb_entry_request_extra_metadata (RhythmDB *db,
3492 RhythmDBEntry *entry,
3493 const gchar *property_name)
3495 GValue *value = NULL;
3497 g_signal_emit (G_OBJECT (db),
3498 rhythmdb_signals[ENTRY_EXTRA_METADATA_REQUEST],
3499 g_quark_from_string (property_name),
3500 entry,
3501 &value);
3503 return value;
3507 * rhythmdb_emit_entry_extra_metadata_notify:
3508 * @db: a #RhythmDB
3509 * @entry: a #RhythmDBEntry
3510 * @property_name: the metadata predicate
3511 * @metadata: a #GValue
3513 * Emits a signal describing extra metadata for the @entry. The @property_name
3514 * argument is emitted as the ::detail part of the
3515 * "entry_extra_metadata_notify" signal and as the 'field' parameter. Handlers
3516 * can ensure they only get metadata they are interested in by supplying an
3517 * appropriate ::detail part when connecting to the signal. If handlers are
3518 * interested in the metadata they should ref or copy the contents of @metadata
3519 * and unref or free it when they are finished with it.
3521 void
3522 rhythmdb_emit_entry_extra_metadata_notify (RhythmDB *db,
3523 RhythmDBEntry *entry,
3524 const gchar *property_name,
3525 const GValue *metadata)
3527 g_signal_emit (G_OBJECT (db),
3528 rhythmdb_signals[ENTRY_EXTRA_METADATA_NOTIFY],
3529 g_quark_from_string (property_name),
3530 entry,
3531 property_name,
3532 metadata);
3535 static void
3536 unset_and_free_g_value (gpointer valpointer)
3538 GValue *value = valpointer;
3539 g_value_unset (value);
3540 g_free (value);
3544 * rhythmdb_entry_extra_gather:
3545 * @db: a #RhythmDB
3546 * @entry: a #RhythmDBEntry
3548 * Gathers all metadata for the @entry. The returned GHashTable maps property
3549 * names and extra metadata names (described under
3550 * @rhythmdb_entry_request_extra_metadata) to GValues. Anything wanting to
3551 * provide extra metadata should connect to the "entry_extra_metadata_gather"
3552 * signal.
3554 * Returns: a GHashTable containing metadata for the entry. This must be freed
3555 * using g_hash_table_destroy.
3557 GHashTable *
3558 rhythmdb_entry_gather_metadata (RhythmDB *db,
3559 RhythmDBEntry *entry)
3561 GHashTable *metadata;
3562 GEnumClass *klass;
3563 guint i;
3565 metadata = g_hash_table_new_full (g_str_hash,
3566 g_str_equal,
3567 g_free,
3568 unset_and_free_g_value);
3570 /* add core properties */
3571 klass = g_type_class_ref (RHYTHMDB_TYPE_PROP_TYPE);
3572 for (i = 0; i < klass->n_values; i++) {
3573 GValue *value;
3574 gint prop;
3575 GType value_type;
3576 const char *name;
3578 prop = klass->values[i].value;
3580 /* only include easily marshallable types in the hash table */
3581 value_type = rhythmdb_get_property_type (db, prop);
3582 switch (value_type) {
3583 case G_TYPE_STRING:
3584 case G_TYPE_BOOLEAN:
3585 case G_TYPE_ULONG:
3586 case G_TYPE_UINT64:
3587 case G_TYPE_DOUBLE:
3588 break;
3589 default:
3590 continue;
3593 value = g_new0 (GValue, 1);
3594 g_value_init (value, value_type);
3595 rhythmdb_entry_get (db, entry, prop, value);
3596 name = (char *)rhythmdb_nice_elt_name_from_propid (db, prop);
3597 g_hash_table_insert (metadata,
3598 (gpointer) g_strdup (name),
3599 value);
3601 g_type_class_unref (klass);
3603 /* gather extra metadata */
3604 g_signal_emit (G_OBJECT (db),
3605 rhythmdb_signals[ENTRY_EXTRA_METADATA_GATHER], 0,
3606 entry,
3607 metadata);
3609 return metadata;
3612 static gboolean
3613 queue_is_empty (GAsyncQueue *queue)
3615 return g_async_queue_length (queue) <= 0;
3619 * rhythmdb_is_busy:
3620 * @db: a #RhythmDB.
3622 * Returns: whether the #RhythmDB has events to process.
3624 gboolean
3625 rhythmdb_is_busy (RhythmDB *db)
3627 return (!db->priv->action_thread_running ||
3628 !queue_is_empty (db->priv->event_queue) ||
3629 !queue_is_empty (db->priv->action_queue) ||
3630 (db->priv->stat_handle != NULL) ||
3631 (db->priv->outstanding_stats != NULL));
3635 * rhythmdb_compute_status_normal:
3636 * @n_songs: the number of tracks.
3637 * @duration: the total duration of the tracks.
3638 * @size: the total size of the tracks.
3639 * @singular: singular form of the format string to use for entries (eg "%d song")
3640 * @plural: plural form of the format string to use for entries (eg "%d songs")
3642 * Creates a string containing the "status" information about a list of tracks.
3643 * The singular and plural strings must be used in a direct ngettext call
3644 * elsewhere in order for them to be marked for translation correctly.
3646 * Returns: the string, which should be freed with g_free.
3648 char *
3649 rhythmdb_compute_status_normal (gint n_songs,
3650 glong duration,
3651 guint64 size,
3652 const char *singular,
3653 const char *plural)
3655 long days, hours, minutes, seconds;
3656 char *songcount = NULL;
3657 char *time = NULL;
3658 char *size_str = NULL;
3659 char *ret;
3660 const char *minutefmt;
3661 const char *hourfmt;
3662 const char *dayfmt;
3664 songcount = g_strdup_printf (ngettext (singular, plural, n_songs), n_songs);
3666 days = duration / (60 * 60 * 24);
3667 hours = (duration / (60 * 60)) - (days * 24);
3668 minutes = (duration / 60) - ((days * 24 * 60) + (hours * 60));
3669 seconds = duration % 60;
3671 minutefmt = ngettext ("%ld minute", "%ld minutes", minutes);
3672 hourfmt = ngettext ("%ld hour", "%ld hours", hours);
3673 dayfmt = ngettext ("%ld day", "%ld days", days);
3674 if (days > 0) {
3675 if (hours > 0)
3676 if (minutes > 0) {
3677 char *fmt;
3678 /* Translators: the format is "X days, X hours and X minutes" */
3679 fmt = g_strdup_printf (_("%s, %s and %s"), dayfmt, hourfmt, minutefmt);
3680 time = g_strdup_printf (fmt, days, hours, minutes);
3681 g_free (fmt);
3682 } else {
3683 char *fmt;
3684 /* Translators: the format is "X days and X hours" */
3685 fmt = g_strdup_printf (_("%s and %s"), dayfmt, hourfmt);
3686 time = g_strdup_printf (fmt, days, hours);
3687 g_free (fmt);
3689 else
3690 if (minutes > 0) {
3691 char *fmt;
3692 /* Translators: the format is "X days and X minutes" */
3693 fmt = g_strdup_printf (_("%s and %s"), dayfmt, minutefmt);
3694 time = g_strdup_printf (fmt, days, minutes);
3695 g_free (fmt);
3696 } else {
3697 time = g_strdup_printf (dayfmt, days);
3699 } else {
3700 if (hours > 0) {
3701 if (minutes > 0) {
3702 char *fmt;
3703 /* Translators: the format is "X hours and X minutes" */
3704 fmt = g_strdup_printf (_("%s and %s"), hourfmt, minutefmt);
3705 time = g_strdup_printf (fmt, hours, minutes);
3706 g_free (fmt);
3707 } else {
3708 time = g_strdup_printf (hourfmt, hours);
3711 } else {
3712 time = g_strdup_printf (minutefmt, minutes);
3716 size_str = gnome_vfs_format_file_size_for_display (size);
3718 if (size > 0 && duration > 0) {
3719 ret = g_strdup_printf ("%s, %s, %s", songcount, time, size_str);
3720 } else if (duration > 0) {
3721 ret = g_strdup_printf ("%s, %s", songcount, time);
3722 } else if (size > 0) {
3723 ret = g_strdup_printf ("%s, %s", songcount, size_str);
3724 } else {
3725 ret = g_strdup (songcount);
3728 g_free (songcount);
3729 g_free (time);
3730 g_free (size_str);
3732 return ret;
3735 static void
3736 default_sync_metadata (RhythmDB *db,
3737 RhythmDBEntry *entry,
3738 GError **error,
3739 gpointer data)
3741 const char *uri;
3742 GError *local_error = NULL;
3744 uri = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
3745 rb_metadata_load (db->priv->metadata,
3746 uri, &local_error);
3747 if (local_error != NULL) {
3748 g_propagate_error (error, local_error);
3749 return;
3752 entry_to_rb_metadata (db, entry, db->priv->metadata);
3754 rb_metadata_save (db->priv->metadata, &local_error);
3755 if (local_error != NULL) {
3756 RhythmDBAction *load_action;
3758 /* reload the metadata, to revert the db changes */
3759 load_action = g_new0 (RhythmDBAction, 1);
3760 load_action->type = RHYTHMDB_ACTION_LOAD;
3761 load_action->uri = rb_refstring_ref (entry->location);
3762 load_action->entry_type = RHYTHMDB_ENTRY_TYPE_INVALID;
3763 g_async_queue_push (db->priv->action_queue, load_action);
3765 g_propagate_error (error, local_error);
3770 * rhythmdb_entry_register_type:
3771 * @db: a #RhythmDB
3772 * @name: optional name for the entry type
3774 * Registers a new #RhythmDBEntryType. This should be called to create a new
3775 * entry type for non-permanent sources.
3777 * Returns: the new #RhythmDBEntryType.
3779 RhythmDBEntryType
3780 rhythmdb_entry_register_type (RhythmDB *db,
3781 const char *name)
3783 RhythmDBEntryType type;
3784 RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
3786 type = g_new0 (RhythmDBEntryType_, 1);
3787 type->can_sync_metadata = (RhythmDBEntryCanSyncFunc)rb_false_function;
3788 type->sync_metadata = default_sync_metadata;
3789 if (name) {
3790 type->name = g_strdup (name);
3791 g_mutex_lock (db->priv->entry_type_map_mutex);
3792 g_hash_table_insert (db->priv->entry_type_map, g_strdup (type->name), type);
3793 g_mutex_unlock (db->priv->entry_type_map_mutex);
3796 if (klass->impl_entry_type_registered)
3797 klass->impl_entry_type_registered (db, name, type);
3799 return type;
3802 static void
3803 rhythmdb_entry_register_type_alias (RhythmDB *db,
3804 RhythmDBEntryType type,
3805 const char *name)
3807 char *dn = g_strdup (name);
3809 g_mutex_lock (db->priv->entry_type_map_mutex);
3810 g_hash_table_insert (db->priv->entry_type_map, dn, type);
3811 g_mutex_unlock (db->priv->entry_type_map_mutex);
3814 typedef struct {
3815 GHFunc func;
3816 gpointer data;
3817 } RhythmDBEntryTypeForeachData;
3819 static void
3820 rhythmdb_entry_type_foreach_cb (const char *name,
3821 RhythmDBEntryType entry_type,
3822 RhythmDBEntryTypeForeachData *data)
3824 /* skip aliases */
3825 if (strcmp (entry_type->name, name))
3826 return;
3828 data->func ((gpointer) name, entry_type, data->data);
3832 * rhythmdb_entry_type_foreach:
3833 * @db: a #RhythmDB
3834 * @func: callback function to call for each registered entry type
3835 * @data: data to pass to the callback
3837 * Calls a function for each registered entry type.
3839 void
3840 rhythmdb_entry_type_foreach (RhythmDB *db,
3841 GHFunc func,
3842 gpointer data)
3844 RhythmDBEntryTypeForeachData d;
3846 d.func = func;
3847 d.data = data;
3849 g_mutex_lock (db->priv->entry_type_mutex);
3850 g_hash_table_foreach (db->priv->entry_type_map,
3851 (GHFunc) rhythmdb_entry_type_foreach_cb,
3852 &d);
3853 g_mutex_unlock (db->priv->entry_type_mutex);
3857 * rhythmdb_entry_type_get_by_name:
3858 * @db: a #RhythmDB
3859 * @name: name of the type to look for
3861 * Locates a #RhythmDBEntryType by name. Returns
3862 * RHYTHMDB_ENTRY_TYPE_INVALID if no entry type
3863 * is registered with the specified name.
3865 * Returns: the #RhythmDBEntryType
3867 RhythmDBEntryType
3868 rhythmdb_entry_type_get_by_name (RhythmDB *db,
3869 const char *name)
3871 gpointer t = NULL;
3873 g_mutex_lock (db->priv->entry_type_map_mutex);
3874 if (db->priv->entry_type_map) {
3875 t = g_hash_table_lookup (db->priv->entry_type_map, name);
3877 g_mutex_unlock (db->priv->entry_type_map_mutex);
3879 if (t)
3880 return (RhythmDBEntryType) t;
3882 return RHYTHMDB_ENTRY_TYPE_INVALID;
3885 static gboolean
3886 song_can_sync_metadata (RhythmDB *db,
3887 RhythmDBEntry *entry,
3888 gpointer data)
3890 const char *mimetype;
3892 mimetype = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MIMETYPE);
3893 return rb_metadata_can_save (db->priv->metadata, mimetype);
3896 static char *
3897 podcast_get_playback_uri (RhythmDBEntry *entry,
3898 gpointer data)
3900 return rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_MOUNTPOINT);
3903 static void
3904 podcast_data_destroy (RhythmDBEntry *entry,
3905 gpointer something)
3907 RhythmDBPodcastFields *podcast = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RhythmDBPodcastFields);
3908 rb_refstring_unref (podcast->description);
3909 rb_refstring_unref (podcast->subtitle);
3910 rb_refstring_unref (podcast->summary);
3911 rb_refstring_unref (podcast->lang);
3912 rb_refstring_unref (podcast->copyright);
3913 rb_refstring_unref (podcast->image);
3916 static RhythmDBEntryType song_type = RHYTHMDB_ENTRY_TYPE_INVALID;
3917 static RhythmDBEntryType ignore_type = RHYTHMDB_ENTRY_TYPE_INVALID;
3918 static RhythmDBEntryType import_error_type = RHYTHMDB_ENTRY_TYPE_INVALID;
3920 /* to be evicted */
3921 static RhythmDBEntryType podcast_post_type = RHYTHMDB_ENTRY_TYPE_INVALID;
3922 static RhythmDBEntryType podcast_feed_type = RHYTHMDB_ENTRY_TYPE_INVALID;
3924 static void
3925 rhythmdb_register_core_entry_types (RhythmDB *db)
3927 /* regular songs */
3928 song_type = rhythmdb_entry_register_type (db, "song");
3929 rhythmdb_entry_register_type_alias (db, song_type, "0");
3930 song_type->save_to_disk = TRUE;
3931 song_type->category = RHYTHMDB_ENTRY_NORMAL;
3932 song_type->can_sync_metadata = song_can_sync_metadata;
3934 /* import errors */
3935 import_error_type = rhythmdb_entry_register_type (db, "import-error");
3936 import_error_type->get_playback_uri = (RhythmDBEntryStringFunc)rb_null_function;
3937 import_error_type->category = RHYTHMDB_ENTRY_VIRTUAL;
3939 /* ignored files */
3940 ignore_type = rhythmdb_entry_register_type (db, "ignore");
3941 ignore_type->save_to_disk = TRUE;
3942 ignore_type->category = RHYTHMDB_ENTRY_VIRTUAL;
3944 /* podcast posts */
3945 podcast_post_type = rhythmdb_entry_register_type (db, "podcast-post");
3946 podcast_post_type->entry_type_data_size = sizeof (RhythmDBPodcastFields);
3947 podcast_post_type->save_to_disk = TRUE;
3948 podcast_post_type->category = RHYTHMDB_ENTRY_NORMAL;
3949 podcast_post_type->pre_entry_destroy = (RhythmDBEntryActionFunc) podcast_data_destroy;
3950 podcast_post_type->get_playback_uri = podcast_get_playback_uri;
3952 /* podcast feeds */
3953 podcast_feed_type = rhythmdb_entry_register_type (db, "podcast-feed");
3954 podcast_feed_type->entry_type_data_size = sizeof (RhythmDBPodcastFields);
3955 podcast_feed_type->save_to_disk = TRUE;
3956 podcast_feed_type->category = RHYTHMDB_ENTRY_VIRTUAL;
3957 podcast_feed_type->pre_entry_destroy = (RhythmDBEntryActionFunc) podcast_data_destroy;
3960 RhythmDBEntryType
3961 rhythmdb_entry_song_get_type (void)
3963 return song_type;
3966 RhythmDBEntryType
3967 rhythmdb_entry_ignore_get_type (void)
3969 return ignore_type;
3972 RhythmDBEntryType
3973 rhythmdb_entry_import_error_get_type (void)
3975 return import_error_type;
3978 RhythmDBEntryType
3979 rhythmdb_entry_podcast_post_get_type (void)
3981 return podcast_post_type;
3984 RhythmDBEntryType
3985 rhythmdb_entry_podcast_feed_get_type (void)
3987 return podcast_feed_type;
3990 static void
3991 rhythmdb_entry_set_mount_point (RhythmDB *db,
3992 RhythmDBEntry *entry,
3993 const gchar *realuri)
3995 gchar *mount_point;
3996 GValue value = {0, };
3998 mount_point = rb_uri_get_mount_point (realuri);
3999 if (mount_point != NULL) {
4000 g_value_init (&value, G_TYPE_STRING);
4001 g_value_set_string_take_ownership (&value, mount_point);
4002 rhythmdb_entry_set_internal (db, entry, FALSE,
4003 RHYTHMDB_PROP_MOUNTPOINT,
4004 &value);
4005 g_value_unset (&value);
4009 void
4010 rhythmdb_entry_set_visibility (RhythmDB *db,
4011 RhythmDBEntry *entry,
4012 gboolean visible)
4014 GValue old_val = {0, };
4015 gboolean old_visible;
4017 g_return_if_fail (RHYTHMDB_IS (db));
4018 g_return_if_fail (entry != NULL);
4020 g_value_init (&old_val, G_TYPE_BOOLEAN);
4022 rhythmdb_entry_get (db, entry, RHYTHMDB_PROP_HIDDEN, &old_val);
4023 old_visible = !g_value_get_boolean (&old_val);
4025 if ((old_visible && !visible) || (!old_visible && visible)) {
4026 GValue new_val = {0, };
4028 g_value_init (&new_val, G_TYPE_BOOLEAN);
4029 g_value_set_boolean (&new_val, !visible);
4030 rhythmdb_entry_set_internal (db, entry, TRUE,
4031 RHYTHMDB_PROP_HIDDEN, &new_val);
4032 g_value_unset (&new_val);
4034 g_value_unset (&old_val);
4037 static gboolean
4038 rhythmdb_idle_save (RhythmDB *db)
4040 if (db->priv->dirty) {
4041 rb_debug ("database is dirty, doing regular save");
4042 rhythmdb_save_async (db);
4045 return TRUE;
4048 static void
4049 rhythmdb_sync_library_location (RhythmDB *db)
4051 gboolean reload = (db->priv->library_locations != NULL);
4053 if (db->priv->library_location_notify_id == 0) {
4054 db->priv->library_location_notify_id =
4055 eel_gconf_notification_add (CONF_LIBRARY_LOCATION,
4056 (GConfClientNotifyFunc) library_location_changed_cb,
4057 db);
4060 if (reload) {
4061 rb_debug ("ending monitor of old library directories");
4063 rhythmdb_stop_monitoring (db);
4065 g_slist_foreach (db->priv->library_locations, (GFunc) g_free, NULL);
4066 g_slist_free (db->priv->library_locations);
4067 db->priv->library_locations = NULL;
4070 if (eel_gconf_get_boolean (CONF_MONITOR_LIBRARY)) {
4071 db->priv->library_locations = eel_gconf_get_string_list (CONF_LIBRARY_LOCATION);
4073 rhythmdb_start_monitoring (db);
4077 static void
4078 library_location_changed_cb (GConfClient *client,
4079 guint cnxn_id,
4080 GConfEntry *entry,
4081 RhythmDB *db)
4083 rhythmdb_sync_library_location (db);
4086 char *
4087 rhythmdb_entry_dup_string (RhythmDBEntry *entry,
4088 RhythmDBPropType propid)
4090 const char *s;
4092 g_return_val_if_fail (entry != NULL, NULL);
4094 s = rhythmdb_entry_get_string (entry, propid);
4095 if (s != NULL) {
4096 return g_strdup (s);
4097 } else {
4098 return NULL;
4102 const char *
4103 rhythmdb_entry_get_string (RhythmDBEntry *entry,
4104 RhythmDBPropType propid)
4106 RhythmDBPodcastFields *podcast = NULL;
4108 g_return_val_if_fail (entry != NULL, NULL);
4109 g_return_val_if_fail (entry->refcount > 0, NULL);
4111 if (entry->type == RHYTHMDB_ENTRY_TYPE_PODCAST_FEED ||
4112 entry->type == RHYTHMDB_ENTRY_TYPE_PODCAST_POST)
4113 podcast = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RhythmDBPodcastFields);
4115 rhythmdb_entry_sync_mirrored (entry, propid);
4117 switch (propid) {
4118 case RHYTHMDB_PROP_TITLE:
4119 return rb_refstring_get (entry->title);
4120 case RHYTHMDB_PROP_ALBUM:
4121 return rb_refstring_get (entry->album);
4122 case RHYTHMDB_PROP_ARTIST:
4123 return rb_refstring_get (entry->artist);
4124 case RHYTHMDB_PROP_GENRE:
4125 return rb_refstring_get (entry->genre);
4126 case RHYTHMDB_PROP_MUSICBRAINZ_TRACKID:
4127 return rb_refstring_get (entry->musicbrainz_trackid);
4128 case RHYTHMDB_PROP_MIMETYPE:
4129 return rb_refstring_get (entry->mimetype);
4130 case RHYTHMDB_PROP_TITLE_SORT_KEY:
4131 return rb_refstring_get_sort_key (entry->title);
4132 case RHYTHMDB_PROP_ALBUM_SORT_KEY:
4133 return rb_refstring_get_sort_key (entry->album);
4134 case RHYTHMDB_PROP_ARTIST_SORT_KEY:
4135 return rb_refstring_get_sort_key (entry->artist);
4136 case RHYTHMDB_PROP_GENRE_SORT_KEY:
4137 return rb_refstring_get_sort_key (entry->genre);
4138 case RHYTHMDB_PROP_TITLE_FOLDED:
4139 return rb_refstring_get_folded (entry->title);
4140 case RHYTHMDB_PROP_ALBUM_FOLDED:
4141 return rb_refstring_get_folded (entry->album);
4142 case RHYTHMDB_PROP_ARTIST_FOLDED:
4143 return rb_refstring_get_folded (entry->artist);
4144 case RHYTHMDB_PROP_GENRE_FOLDED:
4145 return rb_refstring_get_folded (entry->genre);
4146 case RHYTHMDB_PROP_LOCATION:
4147 return rb_refstring_get (entry->location);
4148 case RHYTHMDB_PROP_MOUNTPOINT:
4149 return rb_refstring_get (entry->mountpoint);
4150 case RHYTHMDB_PROP_LAST_PLAYED_STR:
4151 return rb_refstring_get (entry->last_played_str);
4152 case RHYTHMDB_PROP_PLAYBACK_ERROR:
4153 return rb_refstring_get (entry->playback_error);
4154 case RHYTHMDB_PROP_FIRST_SEEN_STR:
4155 return rb_refstring_get (entry->first_seen_str);
4156 case RHYTHMDB_PROP_LAST_SEEN_STR:
4157 return rb_refstring_get (entry->last_seen_str);
4158 case RHYTHMDB_PROP_SEARCH_MATCH:
4159 return NULL; /* synthetic property */
4160 /* Podcast properties */
4161 case RHYTHMDB_PROP_DESCRIPTION:
4162 if (podcast)
4163 return rb_refstring_get (podcast->description);
4164 else
4165 return NULL;
4166 case RHYTHMDB_PROP_SUBTITLE:
4167 if (podcast)
4168 return rb_refstring_get (podcast->subtitle);
4169 else
4170 return NULL;
4171 case RHYTHMDB_PROP_SUMMARY:
4172 if (podcast)
4173 return rb_refstring_get (podcast->summary);
4174 else
4175 return NULL;
4176 case RHYTHMDB_PROP_LANG:
4177 if (podcast)
4178 return rb_refstring_get (podcast->lang);
4179 else
4180 return NULL;
4181 case RHYTHMDB_PROP_COPYRIGHT:
4182 if (podcast)
4183 return rb_refstring_get (podcast->copyright);
4184 else
4185 return NULL;
4186 case RHYTHMDB_PROP_IMAGE:
4187 if (podcast)
4188 return rb_refstring_get (podcast->image);
4189 else
4190 return NULL;
4192 default:
4193 g_assert_not_reached ();
4194 return NULL;
4198 RBRefString *
4199 rhythmdb_entry_get_refstring (RhythmDBEntry *entry,
4200 RhythmDBPropType propid)
4202 g_return_val_if_fail (entry != NULL, NULL);
4203 g_return_val_if_fail (entry->refcount > 0, NULL);
4205 rhythmdb_entry_sync_mirrored (entry, propid);
4207 switch (propid) {
4208 case RHYTHMDB_PROP_TITLE:
4209 return rb_refstring_ref (entry->title);
4210 case RHYTHMDB_PROP_ALBUM:
4211 return rb_refstring_ref (entry->album);
4212 case RHYTHMDB_PROP_ARTIST:
4213 return rb_refstring_ref (entry->artist);
4214 case RHYTHMDB_PROP_GENRE:
4215 return rb_refstring_ref (entry->genre);
4216 case RHYTHMDB_PROP_MUSICBRAINZ_TRACKID:
4217 return rb_refstring_ref (entry->musicbrainz_trackid);
4218 case RHYTHMDB_PROP_MIMETYPE:
4219 return rb_refstring_ref (entry->mimetype);
4220 case RHYTHMDB_PROP_MOUNTPOINT:
4221 return rb_refstring_ref (entry->mountpoint);
4222 case RHYTHMDB_PROP_LAST_PLAYED_STR:
4223 return rb_refstring_ref (entry->last_played_str);
4224 case RHYTHMDB_PROP_FIRST_SEEN_STR:
4225 return rb_refstring_ref (entry->first_seen_str);
4226 case RHYTHMDB_PROP_LAST_SEEN_STR:
4227 return rb_refstring_ref (entry->last_seen_str);
4228 case RHYTHMDB_PROP_LOCATION:
4229 return rb_refstring_ref (entry->location);
4230 case RHYTHMDB_PROP_PLAYBACK_ERROR:
4231 return rb_refstring_ref (entry->playback_error);
4232 default:
4233 g_assert_not_reached ();
4234 return NULL;
4238 gboolean
4239 rhythmdb_entry_get_boolean (RhythmDBEntry *entry,
4240 RhythmDBPropType propid)
4242 g_return_val_if_fail (entry != NULL, FALSE);
4244 switch (propid) {
4245 case RHYTHMDB_PROP_HIDDEN:
4246 return ((entry->flags & RHYTHMDB_ENTRY_HIDDEN) != 0);
4247 default:
4248 g_assert_not_reached ();
4249 return FALSE;
4253 guint64
4254 rhythmdb_entry_get_uint64 (RhythmDBEntry *entry,
4255 RhythmDBPropType propid)
4257 g_return_val_if_fail (entry != NULL, 0);
4259 switch (propid) {
4260 case RHYTHMDB_PROP_FILE_SIZE:
4261 return entry->file_size;
4262 default:
4263 g_assert_not_reached ();
4264 return 0;
4268 RhythmDBEntryType
4269 rhythmdb_entry_get_entry_type (RhythmDBEntry *entry)
4271 g_return_val_if_fail (entry != NULL, RHYTHMDB_ENTRY_TYPE_INVALID);
4273 return entry->type;
4276 gpointer
4277 rhythmdb_entry_get_pointer (RhythmDBEntry *entry,
4278 RhythmDBPropType propid)
4280 g_return_val_if_fail (entry != NULL, NULL);
4282 switch (propid) {
4283 case RHYTHMDB_PROP_TYPE:
4284 return entry->type;
4285 default:
4286 g_assert_not_reached ();
4287 return NULL;
4291 gulong
4292 rhythmdb_entry_get_ulong (RhythmDBEntry *entry,
4293 RhythmDBPropType propid)
4295 RhythmDBPodcastFields *podcast = NULL;
4297 g_return_val_if_fail (entry != NULL, 0);
4299 if (entry->type == RHYTHMDB_ENTRY_TYPE_PODCAST_FEED ||
4300 entry->type == RHYTHMDB_ENTRY_TYPE_PODCAST_POST)
4301 podcast = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RhythmDBPodcastFields);
4303 switch (propid) {
4304 case RHYTHMDB_PROP_ENTRY_ID:
4305 return entry->id;
4306 case RHYTHMDB_PROP_TRACK_NUMBER:
4307 return entry->tracknum;
4308 case RHYTHMDB_PROP_DISC_NUMBER:
4309 return entry->discnum;
4310 case RHYTHMDB_PROP_DURATION:
4311 return entry->duration;
4312 case RHYTHMDB_PROP_MTIME:
4313 return entry->mtime;
4314 case RHYTHMDB_PROP_FIRST_SEEN:
4315 return entry->first_seen;
4316 case RHYTHMDB_PROP_LAST_SEEN:
4317 return entry->last_seen;
4318 case RHYTHMDB_PROP_LAST_PLAYED:
4319 return entry->last_played;
4320 case RHYTHMDB_PROP_PLAY_COUNT:
4321 return entry->play_count;
4322 case RHYTHMDB_PROP_BITRATE:
4323 return entry->bitrate;
4324 case RHYTHMDB_PROP_DATE:
4325 if (g_date_valid (&entry->date))
4326 return g_date_get_julian (&entry->date);
4327 else
4328 return 0;
4329 case RHYTHMDB_PROP_YEAR:
4330 if (g_date_valid (&entry->date))
4331 return g_date_get_year (&entry->date);
4332 else
4333 return 0;
4334 case RHYTHMDB_PROP_POST_TIME:
4335 if (podcast)
4336 return podcast->post_time;
4337 else
4338 return 0;
4339 case RHYTHMDB_PROP_STATUS:
4340 if (podcast)
4341 return podcast->status;
4342 else
4343 return 0;
4344 default:
4345 g_assert_not_reached ();
4346 return 0;
4350 double
4351 rhythmdb_entry_get_double (RhythmDBEntry *entry,
4352 RhythmDBPropType propid)
4354 g_return_val_if_fail (entry != NULL, 0);
4356 switch (propid) {
4357 case RHYTHMDB_PROP_TRACK_GAIN:
4358 return entry->track_gain;
4359 case RHYTHMDB_PROP_TRACK_PEAK:
4360 return entry->track_peak;
4361 case RHYTHMDB_PROP_ALBUM_GAIN:
4362 return entry->album_gain;
4363 case RHYTHMDB_PROP_ALBUM_PEAK:
4364 return entry->album_peak;
4365 case RHYTHMDB_PROP_RATING:
4366 return entry->rating;
4367 default:
4368 g_assert_not_reached ();
4369 return 0.0;
4373 char *
4374 rhythmdb_entry_get_playback_uri (RhythmDBEntry *entry)
4376 RhythmDBEntryType type;
4378 g_return_val_if_fail (entry != NULL, NULL);
4380 type = rhythmdb_entry_get_entry_type (entry);
4381 if (type->get_playback_uri)
4382 return (type->get_playback_uri) (entry, type->get_playback_uri_data);
4383 else
4384 return rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_LOCATION);
4387 GType
4388 rhythmdb_get_property_type (RhythmDB *db,
4389 guint property_id)
4391 g_assert (property_id >= 0 && property_id < RHYTHMDB_NUM_PROPERTIES);
4392 return rhythmdb_property_type_map[property_id];
4395 GType
4396 rhythmdb_entry_get_type (void)
4398 static GType type = 0;
4400 if (G_UNLIKELY (type == 0)) {
4401 type = g_boxed_type_register_static ("RhythmDBEntry",
4402 (GBoxedCopyFunc)rhythmdb_entry_ref,
4403 (GBoxedFreeFunc)rhythmdb_entry_unref);
4406 return type;
4409 GType
4410 rhythmdb_entry_type_get_type (void)
4412 static GType type = 0;
4414 if (G_UNLIKELY (type == 0)) {
4415 type = g_boxed_type_register_static ("RhythmDBEntryType",
4416 (GBoxedCopyFunc)rb_copy_function,
4417 (GBoxedFreeFunc)rb_null_function);
4420 return type;