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