1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2010 Jonathan Matthew <jonathan@d14n.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * The Rhythmbox authors hereby grant permission for non-GPL compatible
11 * GStreamer plugins to be used and distributed together with GStreamer
12 * and Rhythmbox. This permission is above and beyond the permissions granted
13 * by the GPL license by which Rhythmbox is covered. If you modify this code
14 * you may extend this exception to your version of the code, but you are not
15 * obligated to do so. If you do not wish to do so, delete this exception
16 * statement from your version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
26 *
27 */
28
29 #include "config.h"
30
31 #include <string.h>
32
33 #include <glib/gi18n.h>
34
35 #include "rhythmdb-entry-type.h"
36 #include "rhythmdb-metadata-cache.h"
37 #include "rhythmdb-private.h"
38
39 #include "rb-util.h"
40
41
42 enum
43 {
44 PROP_0,
45 PROP_DB,
46 PROP_NAME,
47 PROP_SAVE_TO_DISK,
48 PROP_TYPE_DATA_SIZE,
49 PROP_CATEGORY,
50 PROP_CACHE_NAME
51 };
52
53 static void rhythmdb_entry_type_class_init (RhythmDBEntryTypeClass *klass);
54 static void rhythmdb_entry_type_init (RhythmDBEntryType *etype);
55
56 struct _RhythmDBEntryTypePrivate
57 {
58 RhythmDB *db;
59
60 char *name;
61 gboolean save_to_disk;
62 guint entry_type_data_size;
63 RhythmDBEntryCategory category;
64
65 char *cache_name;
66
67 RhythmDBMetadataCache *cache;
68 };
69
70
G_DEFINE_TYPE(RhythmDBEntryType,rhythmdb_entry_type,G_TYPE_OBJECT)71 G_DEFINE_TYPE (RhythmDBEntryType, rhythmdb_entry_type, G_TYPE_OBJECT)
72
73 /**
74 * SECTION:rhythmdb-entry-type
75 * @short_description: Database entry type base class
76 *
77 * This is the base class for database entry type classes, which provide
78 * some aspects of the behaviour of database entry types. There are different
79 * entry types for songs, radio streams, podcast feeds and episodes, and so on.
80 *
81 * Plugins written in Python or Vala can create new entry types by subclassing
82 * and overriding any methods required. Plugins written in C can create a new
83 * instance of the RhythmDBEntryType base class and use its function pointer
84 * members rather than subclassing.
85 */
86
87 /**
88 * rhythmdb_entry_type_get_name:
89 * @etype: a #RhythmDBEntryType
90 *
91 * Returns the name of the entry type
92 *
93 * Return value: entry type name
94 */
95 const char *
96 rhythmdb_entry_type_get_name (RhythmDBEntryType *etype)
97 {
98 return etype->priv->name;
99 }
100
101 /**
102 * rhythmdb_entry_get_playback_uri:
103 * @entry: a #RhythmDBEntry
104 *
105 * Returns an allocated string containing the playback URI for @entry,
106 * or NULL if the entry cannot be played.
107 *
108 * Return value: playback URI or NULL
109 */
110 char *
rhythmdb_entry_get_playback_uri(RhythmDBEntry * entry)111 rhythmdb_entry_get_playback_uri (RhythmDBEntry *entry)
112 {
113 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
114 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
115
116 if (klass->get_playback_uri) {
117 return (klass->get_playback_uri) (etype, entry);
118 } else {
119 return rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_LOCATION);
120 }
121 }
122
123 /**
124 * rhythmdb_entry_update_availability:
125 * @entry: a #RhythmDBEntry
126 * @avail: an availability event
127 *
128 * Updates @entry to reflect its new availability.
129 */
130 void
rhythmdb_entry_update_availability(RhythmDBEntry * entry,RhythmDBEntryAvailability avail)131 rhythmdb_entry_update_availability (RhythmDBEntry *entry, RhythmDBEntryAvailability avail)
132 {
133 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
134 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
135
136 if (klass->update_availability) {
137 (klass->update_availability) (etype, entry, avail);
138 } else {
139 /* do nothing? */
140 }
141 }
142
143 /**
144 * rhythmdb_entry_created:
145 * @entry: a newly created #RhythmDBEntry
146 *
147 * Calls the entry type's post-creation method for @entry.
148 */
149 void
rhythmdb_entry_created(RhythmDBEntry * entry)150 rhythmdb_entry_created (RhythmDBEntry *entry)
151 {
152 RhythmDBEntryType *etype;
153 RhythmDBEntryTypeClass *klass;
154
155 etype = rhythmdb_entry_get_entry_type (entry);
156 klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
157
158 if (klass->entry_created) {
159 klass->entry_created (etype, entry);
160 }
161 }
162
163 /**
164 * rhythmdb_entry_pre_destroy:
165 * @entry: a #RhythmDBEntry
166 *
167 * Calls the entry type's pre-deletion method for @entry.
168 */
169 void
rhythmdb_entry_pre_destroy(RhythmDBEntry * entry)170 rhythmdb_entry_pre_destroy (RhythmDBEntry *entry)
171 {
172 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
173 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
174 if (klass->destroy_entry) {
175 klass->destroy_entry (etype, entry);
176 }
177 }
178
179 /**
180 * rhythmdb_entry_can_sync_metadata:
181 * @entry: a #RhythmDBEntry
182 *
183 * Calls the entry type's method to check if it can sync metadata for @entry.
184 * Usually this is only true for entries backed by files, where tag-writing is
185 * enabled, and the appropriate tag-writing facilities are available.
186 *
187 * Return value: %TRUE if the entry can be synced
188 */
189 gboolean
rhythmdb_entry_can_sync_metadata(RhythmDBEntry * entry)190 rhythmdb_entry_can_sync_metadata (RhythmDBEntry *entry)
191 {
192 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
193 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
194 if (klass->can_sync_metadata) {
195 return klass->can_sync_metadata (etype, entry);
196 } else {
197 return FALSE;
198 }
199 }
200
201 /**
202 * rhythmdb_entry_sync_metadata:
203 * @entry: a #RhythmDBEntry
204 * @changes: (element-type RB.RhythmDBEntryChange): a list of #RhythmDBEntryChange structures
205 * @error: returns error information
206 *
207 * Calls the entry type's method to sync metadata changes for @entry.
208 */
209 void
rhythmdb_entry_sync_metadata(RhythmDBEntry * entry,GSList * changes,GError ** error)210 rhythmdb_entry_sync_metadata (RhythmDBEntry *entry, GSList *changes, GError **error)
211 {
212 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
213 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
214 if (klass->sync_metadata) {
215 klass->sync_metadata (etype, entry, changes, error);
216 } else {
217 /* default implementation? */
218 }
219 }
220
221 /**
222 * rhythmdb_entry_type_fetch_metadata:
223 * @etype: a #RhythmDBEntryType
224 * @uri: uri of the item to fetch
225 * @metadata: (element-type RhythmDBEntryChange): returns fetched metadata
226 *
227 * Fetches metadata for a URI (not an entry yet, at this point) from a cache, if possible.
228 *
229 * The @metadata array contains RhythmDBEntryChange items with just the 'new' value set.
230 *
231 * Return value: %TRUE if metadata is returned
232 */
233 gboolean
rhythmdb_entry_type_fetch_metadata(RhythmDBEntryType * etype,const char * uri,GArray * metadata)234 rhythmdb_entry_type_fetch_metadata (RhythmDBEntryType *etype, const char *uri, GArray *metadata)
235 {
236 char *key;
237 gboolean result;
238
239 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
240 if (klass->uri_to_cache_key == NULL) {
241 return FALSE;
242 }
243
244 key = klass->uri_to_cache_key (etype, uri);
245 if (key == NULL)
246 return FALSE;
247
248 result = rhythmdb_metadata_cache_load (etype->priv->cache, key, metadata);
249 g_free (key);
250 return result;
251 }
252
253 /**
254 * rhythmdb_entry_cache_metadata:
255 * @entry: a #RhythmDBEntry
256 *
257 * Stores metadata for @entry in the metadata cache (if any) for its entry type.
258 */
259 void
rhythmdb_entry_cache_metadata(RhythmDBEntry * entry)260 rhythmdb_entry_cache_metadata (RhythmDBEntry *entry)
261 {
262 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
263 char *key;
264
265 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
266 if (klass->uri_to_cache_key == NULL) {
267 return;
268 }
269
270 key = klass->uri_to_cache_key (etype, rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION));
271 if (key == NULL)
272 return;
273
274 rhythmdb_metadata_cache_store (etype->priv->cache, key, entry);
275 }
276
277 static RhythmDBPropType default_unknown_properties[] = {
278 RHYTHMDB_PROP_GENRE,
279 RHYTHMDB_PROP_ARTIST,
280 RHYTHMDB_PROP_ALBUM,
281 RHYTHMDB_PROP_COMPOSER
282 };
283
284 /**
285 * rhythmdb_entry_apply_cached_metadata:
286 * @entry: a #RhythmDBEntry
287 * @metadata: (element-type RhythmDBEntryChange): cached metadata to apply
288 *
289 * Applies a set of metadata properties to @entry. The metadata should be in the
290 * form returned by @rhythmdb_entry_type_fetch_metadata.
291 */
292 void
rhythmdb_entry_apply_cached_metadata(RhythmDBEntry * entry,GArray * metadata)293 rhythmdb_entry_apply_cached_metadata (RhythmDBEntry *entry, GArray *metadata)
294 {
295 RhythmDBEntryType *etype = rhythmdb_entry_get_entry_type (entry);
296 RhythmDBEntryChange *fields;
297 GValue unknown = {0,};
298 int i;
299
300 g_value_init (&unknown, G_TYPE_STRING);
301 g_value_set_string (&unknown, _("Unknown"));
302 for (i = 0; i < G_N_ELEMENTS(default_unknown_properties); i++) {
303 rhythmdb_entry_set_internal (etype->priv->db, entry, TRUE, default_unknown_properties[i], &unknown);
304 }
305 g_value_unset (&unknown);
306
307 fields = (RhythmDBEntryChange *)metadata->data;
308 for (i = 0; i < metadata->len; i++) {
309 rhythmdb_entry_set_internal (etype->priv->db, entry, TRUE, fields[i].prop, &fields[i].new);
310 }
311 rhythmdb_commit (etype->priv->db);
312 }
313
314 static gboolean
metadata_key_valid_cb(const char * key,RhythmDBEntryType * etype)315 metadata_key_valid_cb (const char *key, RhythmDBEntryType *etype)
316 {
317 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
318 char *uri;
319 gboolean result = FALSE; /* or maybe true? */
320
321 uri = klass->cache_key_to_uri (etype, key);
322 if (uri != NULL) {
323 RhythmDBEntry *entry;
324 entry = rhythmdb_entry_lookup_by_location (etype->priv->db, uri);
325 result = (entry != NULL);
326 }
327
328 g_free (uri);
329 return result;
330 }
331
332 /**
333 * rhythmdb_entry_type_purge_metadata_cache:
334 * @etype: a #RhythmDBEntryType
335 * @prefix: a cache key prefix to scan
336 * @max_age: maximum age of missing entries to keep
337 */
338 void
rhythmdb_entry_type_purge_metadata_cache(RhythmDBEntryType * etype,const char * prefix,guint64 max_age)339 rhythmdb_entry_type_purge_metadata_cache (RhythmDBEntryType *etype, const char *prefix, guint64 max_age)
340 {
341 RhythmDBEntryTypeClass *klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
342 g_assert (klass->cache_key_to_uri != NULL);
343 g_assert (etype->priv->cache != NULL);
344
345 rhythmdb_metadata_cache_purge (etype->priv->cache,
346 prefix,
347 max_age,
348 (RhythmDBMetadataCacheValidFunc) metadata_key_valid_cb,
349 etype,
350 NULL);
351 }
352
353 static void
rhythmdb_entry_type_init(RhythmDBEntryType * etype)354 rhythmdb_entry_type_init (RhythmDBEntryType *etype)
355 {
356 etype->priv = G_TYPE_INSTANCE_GET_PRIVATE (etype,
357 RHYTHMDB_TYPE_ENTRY_TYPE,
358 RhythmDBEntryTypePrivate);
359 }
360
361 static void
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)362 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
363 {
364 RhythmDBEntryType *etype = RHYTHMDB_ENTRY_TYPE (object);
365
366 switch (prop_id) {
367 case PROP_DB:
368 etype->priv->db = g_value_get_object (value);
369 break;
370 case PROP_NAME:
371 etype->priv->name = g_value_dup_string (value);
372 break;
373 case PROP_SAVE_TO_DISK:
374 etype->priv->save_to_disk = g_value_get_boolean (value);
375 break;
376 case PROP_TYPE_DATA_SIZE:
377 etype->priv->entry_type_data_size = g_value_get_uint (value);
378 break;
379 case PROP_CATEGORY:
380 etype->priv->category = g_value_get_enum (value);
381 break;
382 case PROP_CACHE_NAME:
383 etype->priv->cache_name = g_value_dup_string (value);
384 break;
385 default:
386 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
387 break;
388 }
389 }
390
391 static void
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)392 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
393 {
394 RhythmDBEntryType *etype = RHYTHMDB_ENTRY_TYPE (object);
395
396 switch (prop_id) {
397 case PROP_DB:
398 g_value_set_object (value, etype->priv->db);
399 break;
400 case PROP_NAME:
401 g_value_set_string (value, etype->priv->name);
402 break;
403 case PROP_SAVE_TO_DISK:
404 g_value_set_boolean (value, etype->priv->save_to_disk);
405 break;
406 case PROP_TYPE_DATA_SIZE:
407 g_value_set_uint (value, etype->priv->entry_type_data_size);
408 break;
409 case PROP_CATEGORY:
410 g_value_set_enum (value, etype->priv->category);
411 break;
412 case PROP_CACHE_NAME:
413 g_value_set_string (value, etype->priv->cache_name);
414 break;
415 default:
416 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
417 break;
418 }
419 }
420
421 static void
impl_constructed(GObject * object)422 impl_constructed (GObject *object)
423 {
424 RhythmDBEntryType *etype;
425 RhythmDBEntryTypeClass *klass;
426
427 RB_CHAIN_GOBJECT_METHOD (rhythmdb_entry_type_parent_class, constructed, object);
428
429 etype = RHYTHMDB_ENTRY_TYPE (object);
430 klass = RHYTHMDB_ENTRY_TYPE_GET_CLASS (etype);
431
432 if (etype->priv->cache_name) {
433 g_assert (klass->uri_to_cache_key != NULL);
434
435 etype->priv->cache = rhythmdb_metadata_cache_get (etype->priv->db, etype->priv->cache_name);
436 }
437 }
438
439 static void
impl_dispose(GObject * object)440 impl_dispose (GObject *object)
441 {
442 RhythmDBEntryType *etype = RHYTHMDB_ENTRY_TYPE (object);
443
444 g_clear_object (&etype->priv->cache);
445
446 G_OBJECT_CLASS (rhythmdb_entry_type_parent_class)->dispose (object);
447 }
448
449 static void
impl_finalize(GObject * object)450 impl_finalize (GObject *object)
451 {
452 RhythmDBEntryType *etype = RHYTHMDB_ENTRY_TYPE (object);
453
454 g_free (etype->priv->name);
455 g_free (etype->priv->cache_name);
456
457 G_OBJECT_CLASS (rhythmdb_entry_type_parent_class)->finalize (object);
458 }
459
460 static void
rhythmdb_entry_type_class_init(RhythmDBEntryTypeClass * klass)461 rhythmdb_entry_type_class_init (RhythmDBEntryTypeClass *klass)
462 {
463 GObjectClass *object_class = G_OBJECT_CLASS (klass);
464
465 object_class->set_property = impl_set_property;
466 object_class->get_property = impl_get_property;
467 object_class->constructed = impl_constructed;
468 object_class->dispose = impl_dispose;
469 object_class->finalize = impl_finalize;
470
471 /**
472 * RhythmDBEntryType:db:
473 *
474 * The #RhythmDB instance.
475 */
476 g_object_class_install_property (object_class,
477 PROP_DB,
478 g_param_spec_object ("db",
479 "RhythmDB",
480 "RhythmDB instance",
481 RHYTHMDB_TYPE,
482 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
483
484 /**
485 * RhythmDBEntryType:name:
486 *
487 * Entry type name. This must be unique.
488 */
489 g_object_class_install_property (object_class,
490 PROP_NAME,
491 g_param_spec_string ("name",
492 "name",
493 "entry type name",
494 NULL,
495 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
496
497 /**
498 * RhythmDBEntryType:save-to-disk:
499 *
500 * If %TRUE, entries of this type should be written to the
501 * on-disk database.
502 */
503 g_object_class_install_property (object_class,
504 PROP_SAVE_TO_DISK,
505 g_param_spec_boolean ("save-to-disk",
506 "save to disk",
507 "whether to save this type of entry to disk",
508 FALSE,
509 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
510 /**
511 * RhythmDBEntryType:type-data-size:
512 *
513 * The size of the type-specific data structure to allocate for each
514 * entry of this type.
515 */
516 g_object_class_install_property (object_class,
517 PROP_TYPE_DATA_SIZE,
518 g_param_spec_uint ("type-data-size",
519 "type data size",
520 "size of entry type specific data",
521 0, G_MAXUINT, 0,
522 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
523 /**
524 * RhythmDBEntryType:category:
525 *
526 * The #RhythmDBEntryCategory that this entry type fits into.
527 */
528 g_object_class_install_property (object_class,
529 PROP_CATEGORY,
530 g_param_spec_enum ("category",
531 "category",
532 "RhythmDBEntryCategory for the entry type",
533 RHYTHMDB_TYPE_ENTRY_CATEGORY,
534 RHYTHMDB_ENTRY_NORMAL,
535 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
536 /**
537 * RhythmDBEntryType:cache-name:
538 *
539 * Metadata cache name. For entry types created by a plugin, should match the plugin name.
540 * If this is set, the entry type must also implement the uri_to_cache_key method.
541 */
542 g_object_class_install_property (object_class,
543 PROP_CACHE_NAME,
544 g_param_spec_string ("cache-name",
545 "cache name",
546 "metadata cache name",
547 NULL,
548 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
549
550 g_type_class_add_private (klass, sizeof (RhythmDBEntryTypePrivate));
551 }
552
553
554
555 #define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
556
557 /**
558 * RhythmDBEntryCategory:
559 * @RHYTHMDB_ENTRY_NORMAL: Normal files on disk
560 * @RHYTHMDB_ENTRY_STREAM: Endless streams (eg shoutcast)
561 * @RHYTHMDB_ENTRY_CONTAINER: Containers for other entries (eg podcast feeds)
562 * @RHYTHMDB_ENTRY_VIRTUAL: Things Rhythmbox shouldn't normally deal with
563 *
564 * Categories used to group entry types. These are used in a few places to control
565 * handling of entries.
566 */
567
568 GType
rhythmdb_entry_category_get_type(void)569 rhythmdb_entry_category_get_type (void)
570 {
571 static GType etype = 0;
572
573 if (etype == 0)
574 {
575 static const GEnumValue values[] =
576 {
577 ENUM_ENTRY (RHYTHMDB_ENTRY_NORMAL, "normal"),
578 ENUM_ENTRY (RHYTHMDB_ENTRY_STREAM, "stream"),
579 ENUM_ENTRY (RHYTHMDB_ENTRY_CONTAINER, "container"),
580 ENUM_ENTRY (RHYTHMDB_ENTRY_VIRTUAL, "virtual"),
581 { 0, 0, 0 }
582 };
583
584 etype = g_enum_register_static ("RhythmDBEntryCategory", values);
585 }
586
587 return etype;
588 }
589
590 /**
591 * RhythmDBEntryAvailability:
592 * @RHYTHMDB_ENTRY_AVAIL_CHECKED: File was checked and found present
593 * @RHYTHMDB_ENTRY_AVAIL_MOUNTED: Filesystem holding the file was mounted
594 * @RHYTHMDB_ENTRY_AVAIL_UNMOUNTED: Filesystem holding the file was unmounted
595 * @RHYTHMDB_ENTRY_AVAIL_NOT_FOUND: File was checked or played and could not be found
596 *
597 * Various events that can result in changes to the entry's availability.
598 */
599
600 GType
rhythmdb_entry_availability_get_type(void)601 rhythmdb_entry_availability_get_type (void)
602 {
603 static GType etype = 0;
604
605 if (etype == 0)
606 {
607 static const GEnumValue values[] =
608 {
609 ENUM_ENTRY (RHYTHMDB_ENTRY_AVAIL_CHECKED, "checked"),
610 ENUM_ENTRY (RHYTHMDB_ENTRY_AVAIL_MOUNTED, "mounted"),
611 ENUM_ENTRY (RHYTHMDB_ENTRY_AVAIL_UNMOUNTED, "unmounted"),
612 ENUM_ENTRY (RHYTHMDB_ENTRY_AVAIL_NOT_FOUND, "not-found"),
613 { 0, 0, 0 }
614 };
615
616 etype = g_enum_register_static ("RhythmDBEntryAvailability", values);
617 }
618
619 return etype;
620 }
621