1 /*
2 * Copyright (C) 2012 W. Michael Petullo.
3 *
4 * Contact: W. Michael Petullo <mike@flyn.org>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public License
8 * as published by the Free Software Foundation; version 2.1 of
9 * the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 /* This DAAP database implementation maintains a series of hash tables that
22 * represent sets of media. The tables include: root, albums, and artists.
23 * Root contains albums and artists, and albums and artists each contain
24 * the set of albums and artists in the database, respectively. Thus this
25 * database implementation imposes a hierarchical structure, whereas DAAP
26 * normally provides a flat structure.
27 *
28 * Each hash table/set is a mapping between a GrlMedia container and a series of
29 * GrlMedia objects (either more GrlMedia container objects, or, in the case of a
30 * leaf, GrlMediaAudio objects). The constant GrlMedia container objects (e.g.,
31 * albums_container and artists_container) facilitate this, along with additional
32 * GrlMediaAudio objects that the grl_daap_db_add function creates.
33 *
34 * An application will normally first browse using the NULL container,
35 * and thus will first receive albums_container and artists_container. Browsing
36 * albums_container will provide the application the GrlMedia container objects in
37 * albums. Further browsing one of these objects will provide the
38 * application with the songs contained therein.
39 *
40 * Grilo IDs must be unique. Here the convention is:
41 *
42 * 1. Top-level IDs resemble their name (e.g., Album's ID is "albums").
43 * 2. The ID for albums, artists, etc. is the item name prefixed by
44 * the category name (e.g., albums-NAME-OF-ALBUM).
45 * 3. The ID for songs is the string form of the integer identifier used
46 * to identify the song to libdmapsharing.
47 */
48
49 #ifdef HAVE_CONFIG_H
50 #include "config.h"
51 #endif
52
53 #include <glib/gi18n-lib.h>
54 #include <sys/stat.h>
55 #include <sys/types.h>
56 #include <glib.h>
57 #include <grilo.h>
58 #include <string.h>
59 #include <libdmapsharing/dmap.h>
60
61 #include "grl-daap-compat.h"
62 #include "grl-common.h"
63 #include "grl-daap-db.h"
64
65 #define ALBUMS_ID "albums"
66 #define ALBUMS_NAME _("Albums")
67 #define ARTISTS_ID "artists"
68 #define ARTISTS_NAME _("Artists")
69
70 /* Media ID's start at max and go down. Container ID's start at 1 and go up. */
71 static guint nextid = G_MAXINT; /* NOTE: this should be G_MAXUINT, but iPhoto can't handle it. */
72
73 struct GrlDaapDbPrivate {
74 /* Contains each album container (tracked with albums hash table) */
75 GrlMedia *albums_container;
76
77 /* Contains each artist container (tracked with artist hash table) */
78 GrlMedia *artists_container;
79
80 GHashTable *root;
81 GHashTable *albums;
82 GHashTable *artists;
83 };
84
85 enum {
86 PROP_0,
87 PROP_RECORD_FACTORY,
88 };
89
90 static guint
container_hash(gconstpointer a)91 container_hash (gconstpointer a)
92 {
93 return g_str_hash (grl_media_get_id (GRL_MEDIA (a)));
94 }
95
96 static gboolean
container_equal(gconstpointer a,gconstpointer b)97 container_equal (gconstpointer a, gconstpointer b)
98 {
99 return g_str_equal (grl_media_get_id (GRL_MEDIA (a)), grl_media_get_id (GRL_MEDIA (b)));
100 }
101
102 GrlDaapDb *
grl_daap_db_new(void)103 grl_daap_db_new (void)
104 {
105 GrlDaapDb *db = g_object_new (TYPE_GRL_DAAP_DB, NULL);
106
107 return db;
108 }
109
110 static DmapRecord *
grl_daap_db_lookup_by_id(const DmapDb * db,guint id)111 grl_daap_db_lookup_by_id (const DmapDb *db, guint id)
112 {
113 g_error ("Not implemented");
114 return NULL;
115 }
116
117 static void
grl_daap_db_foreach(const DmapDb * db,DmapIdRecordFunc func,gpointer data)118 grl_daap_db_foreach (const DmapDb *db,
119 DmapIdRecordFunc func,
120 gpointer data)
121 {
122 g_error ("Not implemented");
123 }
124
125 static gint64
grl_daap_db_count(const DmapDb * db)126 grl_daap_db_count (const DmapDb *db)
127 {
128 g_error ("Not implemented");
129 return 0;
130 }
131
132 static void
set_insert(GHashTable * category,const char * category_name,char * set_name,GrlMedia * media)133 set_insert (GHashTable *category, const char *category_name, char *set_name, GrlMedia *media)
134 {
135 gchar *id = NULL;
136 GrlMedia *container;
137 GHashTable *set;
138
139 id = g_strdup_printf ("%s-%s", category_name, set_name);
140
141 container = grl_media_container_new ();
142 grl_media_set_id (container, id);
143 grl_media_set_title (container, set_name);
144
145 set = g_hash_table_lookup (category, container);
146 if (NULL == set) {
147 set = g_hash_table_new_full (container_hash, container_equal, g_object_unref, NULL);
148 g_hash_table_insert (category, g_object_ref (container), set);
149 }
150
151 g_hash_table_insert (set, g_object_ref (media), NULL);
152
153 g_free (id);
154 g_object_unref (container);
155 }
156
157 guint
grl_daap_db_add(DmapDb * _db,DmapRecord * _record,GError ** error)158 grl_daap_db_add (DmapDb *_db, DmapRecord *_record, GError **error)
159 {
160 g_assert (GRL_IS_DAAP_DB (_db));
161 g_assert (DMAP_IS_AV_RECORD (_record));
162
163 GrlDaapDb *db = GRL_DAAP_DB (_db);
164 DmapAvRecord *record = DMAP_AV_RECORD (_record);
165
166 gint duration = 0;
167 gint32 bitrate = 0,
168 disc = 0,
169 track = 0;
170 gchar *id_s = NULL,
171 *title = NULL,
172 *album = NULL,
173 *artist = NULL,
174 *genre = NULL,
175 *url = NULL;
176 gboolean has_video;
177 GrlMedia *media;
178
179 g_object_get (record,
180 "songalbum", &album,
181 "songartist", &artist,
182 "bitrate", &bitrate,
183 "duration", &duration,
184 "songgenre", &genre,
185 "title", &title,
186 "track", &track,
187 "disc", &disc,
188 "location", &url,
189 "has-video", &has_video,
190 NULL);
191
192 id_s = g_strdup_printf ("%u", nextid);
193
194 if (has_video == TRUE) {
195 media = grl_media_video_new ();
196 } else {
197 media = grl_media_audio_new ();
198 }
199
200 grl_media_set_id (media, id_s);
201 grl_media_set_duration (media, duration);
202
203 if (title) {
204 grl_media_set_title (media, title);
205 }
206
207 if (url) {
208 /* Replace URL's daap:// with http:// */
209 url[0] = 'h'; url[1] = 't'; url[2] = 't'; url[3] = 'p';
210 grl_media_set_url (media, url);
211 }
212
213 if (has_video == FALSE) {
214 grl_media_set_bitrate (media, bitrate);
215 grl_media_set_track_number (media, track);
216
217 if (disc != 0) {
218 grl_media_set_album_disc_number (media, disc);
219 }
220
221 if (album) {
222 grl_media_set_album (media, album);
223 }
224
225 if (artist) {
226 grl_media_set_artist (media, artist);
227 }
228
229 if (genre) {
230 grl_media_set_genre (media, genre);
231 }
232 }
233
234 set_insert (db->priv->artists, ARTISTS_ID, artist, media);
235 set_insert (db->priv->albums, ALBUMS_ID, album, media);
236
237 g_free (id_s);
238 g_object_unref (media);
239 g_free (album);
240 g_free (artist);
241 g_free (genre);
242 g_free (title);
243 g_free (url);
244
245 return --nextid;
246 }
247
248 static gboolean
same_media(GrlMedia * a,GrlMedia * b)249 same_media (GrlMedia *a, GrlMedia *b)
250 {
251 return strcmp (grl_media_get_id (a), grl_media_get_id (b)) == 0;
252 }
253
254 void
grl_daap_db_browse(GrlDaapDb * db,GrlMedia * container,GrlSource * source,guint op_id,guint skip,guint count,GrlSourceResultCb func,gpointer user_data)255 grl_daap_db_browse (GrlDaapDb *db,
256 GrlMedia *container,
257 GrlSource *source,
258 guint op_id,
259 guint skip,
260 guint count,
261 GrlSourceResultCb func,
262 gpointer user_data)
263 {
264 g_assert (GRL_IS_DAAP_DB (db));
265
266 int i;
267 guint remaining;
268 GHashTable *hash_table;
269 GHashTableIter iter;
270 gpointer key, val;
271
272 const gchar *container_id = grl_media_get_id (container);
273 if (NULL == container_id) {
274 hash_table = db->priv->root;
275 } else if (same_media (container, GRL_MEDIA (db->priv->albums_container))) {
276 hash_table = db->priv->albums;
277 } else if (same_media (container, GRL_MEDIA (db->priv->artists_container))) {
278 hash_table = db->priv->artists;
279 } else {
280 hash_table = g_hash_table_lookup (db->priv->artists, container);
281 if (NULL == hash_table) {
282 hash_table = g_hash_table_lookup (db->priv->albums, container);
283 }
284 }
285
286 /* Should not be NULL; this means the container requested
287 does not exist in the database. */
288 if (NULL == hash_table) {
289 GError *error = g_error_new (GRL_CORE_ERROR,
290 GRL_CORE_ERROR_BROWSE_FAILED,
291 _("Invalid container identifier %s"),
292 container_id);
293 func (source, op_id, NULL, 0, user_data, error);
294 goto done;
295 }
296
297 remaining = g_hash_table_size (hash_table) - skip;
298 remaining = remaining < count ? remaining : count;
299 g_hash_table_iter_init (&iter, hash_table);
300 for (i = 0; g_hash_table_iter_next (&iter, &key, &val) && i < skip + count; i++) {
301 if (i < skip) {
302 continue;
303 }
304 if (grl_media_is_container (key)) {
305 grl_media_set_childcount (key, g_hash_table_size (val));
306 }
307 func (source, op_id, GRL_MEDIA (g_object_ref (key)), --remaining, user_data, NULL);
308 }
309 done:
310 return;
311 }
312
313 void
grl_daap_db_search(GrlDaapDb * db,GrlSource * source,guint op_id,GHRFunc predicate,gpointer pred_user_data,GrlSourceResultCb func,gpointer user_data)314 grl_daap_db_search (GrlDaapDb *db,
315 GrlSource *source,
316 guint op_id,
317 GHRFunc predicate,
318 gpointer pred_user_data,
319 GrlSourceResultCb func,
320 gpointer user_data)
321 {
322 g_assert (GRL_IS_DAAP_DB (db));
323
324 gint i, j, k;
325 guint remaining = 0;
326 gpointer key1, val1, key2, val2;
327 GHashTable *hash_tables[] = { db->priv->albums, db->priv->artists };
328
329 /* Use hash table to avoid duplicates */
330 GHashTable *results = NULL;
331 GHashTableIter iter1, iter2;
332
333 results = g_hash_table_new (g_str_hash, g_str_equal);
334
335 /* For albums and artists... */
336 for (i = 0; i < G_N_ELEMENTS (hash_tables); i++) {
337 g_hash_table_iter_init (&iter1, hash_tables[i]);
338 /* For each album or artist in above... */
339 for (j = 0; g_hash_table_iter_next (&iter1, &key1, &val1); j++) {
340 if (grl_media_is_container (key1)) {
341 g_hash_table_iter_init (&iter2, val1);
342 /* For each media item in above... */
343 for (k = 0; g_hash_table_iter_next (&iter2, &key2, &val2); k++) {
344 const char *id = grl_media_get_id (GRL_MEDIA (key2));
345 /* If the predicate returns true, add to results set. */
346 if (predicate (key2, val2, pred_user_data)
347 && ! g_hash_table_contains (results, id)) {
348 remaining++;
349 g_hash_table_insert (results, (gpointer) id, key2);
350 }
351 }
352 }
353 }
354 }
355
356 /* Process results set. */
357 g_hash_table_iter_init (&iter1, results);
358 for (i = 0; g_hash_table_iter_next (&iter1, &key1, &val1); i++) {
359 func (source, op_id, GRL_MEDIA (g_object_ref (val1)), --remaining, user_data, NULL);
360 }
361 }
362
363 static void
dmap_db_interface_init(gpointer iface,gpointer data)364 dmap_db_interface_init (gpointer iface, gpointer data)
365 {
366 DmapDbInterface *daap_db = iface;
367
368 g_assert (G_TYPE_FROM_INTERFACE (daap_db) == DMAP_TYPE_DB);
369
370 daap_db->add = grl_daap_db_add_compat;
371 daap_db->lookup_by_id = grl_daap_db_lookup_by_id;
372 daap_db->foreach = grl_daap_db_foreach;
373 daap_db->count = grl_daap_db_count;
374 }
375
G_DEFINE_TYPE_WITH_CODE(GrlDaapDb,grl_daap_db,G_TYPE_OBJECT,G_ADD_PRIVATE (GrlDaapDb)G_IMPLEMENT_INTERFACE (DMAP_TYPE_DB,dmap_db_interface_init))376 G_DEFINE_TYPE_WITH_CODE (GrlDaapDb, grl_daap_db, G_TYPE_OBJECT,
377 G_ADD_PRIVATE (GrlDaapDb)
378 G_IMPLEMENT_INTERFACE (DMAP_TYPE_DB, dmap_db_interface_init))
379
380 static GObject*
381 grl_daap_db_constructor (GType type, guint n_construct_params, GObjectConstructParam *construct_params)
382 {
383 GObject *object;
384
385 object = G_OBJECT_CLASS (grl_daap_db_parent_class)->constructor (type, n_construct_params, construct_params);
386
387 return object;
388 }
389
390 static void
grl_daap_db_init(GrlDaapDb * db)391 grl_daap_db_init (GrlDaapDb *db)
392 {
393 db->priv = grl_daap_db_get_instance_private (db);
394
395 db->priv->albums_container = grl_media_container_new ();
396 db->priv->artists_container = grl_media_container_new ();
397
398 grl_media_set_id (GRL_MEDIA (db->priv->albums_container), ALBUMS_ID);
399 grl_media_set_title (GRL_MEDIA (db->priv->albums_container), ALBUMS_NAME);
400
401 grl_media_set_id (GRL_MEDIA (db->priv->artists_container), ARTISTS_ID);
402 grl_media_set_title (GRL_MEDIA (db->priv->artists_container), ARTISTS_NAME);
403
404 db->priv->root = g_hash_table_new_full (container_hash, container_equal, g_object_unref, (GDestroyNotify) g_hash_table_destroy);
405 db->priv->albums = g_hash_table_new_full (container_hash, container_equal, g_object_unref, (GDestroyNotify) g_hash_table_destroy);
406 db->priv->artists = g_hash_table_new_full (container_hash, container_equal, g_object_unref, (GDestroyNotify) g_hash_table_destroy);
407
408 g_hash_table_insert (db->priv->root, g_object_ref (db->priv->albums_container), db->priv->albums);
409 g_hash_table_insert (db->priv->root, g_object_ref (db->priv->artists_container), db->priv->artists);
410 }
411
412 static void
grl_daap_db_finalize(GObject * object)413 grl_daap_db_finalize (GObject *object)
414 {
415 GrlDaapDb *db = GRL_DAAP_DB (object);
416
417 GRL_DEBUG ("Finalizing GrlDaapDb");
418
419 g_object_unref (db->priv->albums_container);
420 g_object_unref (db->priv->artists_container);
421
422 g_hash_table_destroy (db->priv->albums);
423 g_hash_table_destroy (db->priv->artists);
424 }
425
426 static void
grl_daap_db_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)427 grl_daap_db_set_property (GObject *object,
428 guint prop_id,
429 const GValue *value,
430 GParamSpec *pspec)
431 {
432 switch (prop_id) {
433 default:
434 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
435 break;
436 }
437 }
438
439 static void
grl_daap_db_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)440 grl_daap_db_get_property (GObject *object,
441 guint prop_id,
442 GValue *value,
443 GParamSpec *pspec)
444 {
445 switch (prop_id) {
446 default:
447 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
448 break;
449 }
450 }
451
452
453 static void
grl_daap_db_class_init(GrlDaapDbClass * klass)454 grl_daap_db_class_init (GrlDaapDbClass *klass)
455 {
456 GObjectClass *object_class = G_OBJECT_CLASS (klass);
457
458 object_class->finalize = grl_daap_db_finalize;
459 object_class->constructor = grl_daap_db_constructor;
460 object_class->set_property = grl_daap_db_set_property;
461 object_class->get_property = grl_daap_db_get_property;
462 }
463