grl-podcasts.c 53 KB
Newer Older
1
/*
2
 * Copyright (C) 2010, 2011 Igalia S.L.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
 *
 * Contact: Iago Toral Quiroga <itoral@igalia.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

27 28
#include <glib.h>
#include <glib/gstdio.h>
29
#include <glib/gi18n-lib.h>
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
30
#include <grilo.h>
31
#include <net/grl-net.h>
32
#include <libxml/xpath.h>
33 34
#include <sqlite3.h>
#include <string.h>
35
#include <totem-pl-parser.h>
36

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
37
#include "grl-podcasts.h"
38

39 40
#define GRL_ROOT_TITLE "Podcasts"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
41
/* --------- Logging  -------- */
42

43 44
#define GRL_LOG_DOMAIN_DEFAULT podcasts_log_domain
GRL_LOG_DOMAIN_STATIC(podcasts_log_domain);
45 46 47

/* --- Database --- */

48
#define GRL_SQL_DB "grl-podcasts.db"
49

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
50 51 52 53 54 55
#define GRL_SQL_CREATE_TABLE_PODCASTS           \
  "CREATE TABLE IF NOT EXISTS podcasts ("       \
  "id    INTEGER  PRIMARY KEY AUTOINCREMENT,"   \
  "title TEXT,"                                 \
  "url   TEXT,"                                 \
  "desc  TEXT,"                                 \
56 57
  "last_refreshed DATE,"                        \
  "image TEXT)"
58

59 60 61 62 63 64 65 66
#define GRL_SQL_CREATE_TABLE_STREAMS		 \
  "CREATE TABLE IF NOT EXISTS streams ( "        \
  "podcast INTEGER REFERENCES podcasts (id), "   \
  "url     TEXT, "				 \
  "title   TEXT, "                               \
  "length  INTEGER, "                            \
  "mime    TEXT, "                               \
  "date    TEXT, "                               \
67 68
  "desc    TEXT, "                               \
  "image   TEXT)"
69

70 71 72 73 74 75
#define GRL_SQL_GET_PODCASTS				\
  "SELECT p.*, count(s.podcast <> '') "			\
  "FROM podcasts p LEFT OUTER JOIN streams s "		\
  "  ON p.id = s.podcast "				\
  "GROUP BY p.id "					\
  "LIMIT %u OFFSET %u"
76

77 78 79 80 81 82
#define GRL_SQL_GET_PODCASTS_BY_QUERY				\
  "SELECT p.*, count(s.podcast <> '') "				\
  "FROM podcasts p LEFT OUTER JOIN streams s "			\
  "  ON p.id = s.podcast "					\
  "WHERE %s "							\
  "GROUP BY p.id "						\
83 84
  "LIMIT %u OFFSET %u"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
85 86 87
#define GRL_SQL_GET_PODCAST_BY_ID               \
  "SELECT * FROM podcasts "                     \
  "WHERE id='%s' "                              \
88 89
  "LIMIT 1"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
90 91 92
#define GRL_SQL_STORE_PODCAST                   \
  "INSERT INTO podcasts "                       \
  "(url, title, desc) "                         \
93 94
  "VALUES (?, ?, ?)"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
95 96
#define GRL_SQL_REMOVE_PODCAST                  \
  "DELETE FROM podcasts "                       \
97 98
  "WHERE id='%s'"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
99 100
#define GRL_SQL_REMOVE_STREAM                   \
  "DELETE FROM streams "                        \
101 102
  "WHERE url='%s'"

103 104 105 106
#define GRL_SQL_STORE_STREAM                                    \
  "INSERT INTO streams "                                        \
  "(podcast, url, title, length, mime, date, desc, image) "     \
  "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
107

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
108
#define GRL_SQL_DELETE_PODCAST_STREAMS          \
109 110
  "DELETE FROM streams WHERE podcast='%s'"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
111 112 113
#define GRL_SQL_GET_PODCAST_STREAMS             \
  "SELECT * FROM streams "                      \
  "WHERE podcast='%s' "                         \
114 115
  "LIMIT %u  OFFSET %u"

116 117 118 119 120 121 122 123
#define GRL_SQL_GET_PODCAST_STREAMS_BY_TEXT                     \
  "SELECT s.* "                                                 \
  "FROM streams s LEFT OUTER JOIN podcasts p "			\
  "  ON s.podcast = p.id "					\
  "WHERE s.title LIKE '%%%s%%' OR s.desc LIKE '%%%s%%' "	\
  "  OR p.title LIKE '%%%s%%' OR p.desc LIKE '%%%s%%' "         \
  "LIMIT %u OFFSET %u"

124 125 126 127
#define GRL_SQL_GET_PODCAST_STREAMS_ALL         \
  "SELECT * FROM streams "                      \
  "LIMIT %u OFFSET %u"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
128 129 130
#define GRL_SQL_GET_PODCAST_STREAM              \
  "SELECT * FROM streams "                      \
  "WHERE url='%s' "                             \
131 132
  "LIMIT 1"

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
133
#define GRL_SQL_TOUCH_PODCAST			\
134
  "UPDATE podcasts "				\
135 136 137 138
  "SET last_refreshed=?, "			\
  "    desc=?, "                                \
  "    image=? "                                \
  "WHERE id=?"
139 140 141

/* --- Other --- */

142
#define DEFAULT_CACHE_TIME (24 * 60 * 60)
143 144 145

/* --- Plugin information --- */

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
146
#define SOURCE_ID   "grl-podcasts"
147
#define SOURCE_NAME "Podcasts"
148
#define SOURCE_DESC _("A source for browsing podcasts")
149

150 151 152 153 154 155
enum {
  PODCAST_ID = 0,
  PODCAST_TITLE,
  PODCAST_URL,
  PODCAST_DESC,
  PODCAST_LAST_REFRESHED,
156
  PODCAST_IMAGE,
157
  PODCAST_LAST,
158 159 160 161 162 163 164 165 166 167
};

enum {
  STREAM_PODCAST = 0,
  STREAM_URL,
  STREAM_TITLE,
  STREAM_LENGTH,
  STREAM_MIME,
  STREAM_DATE,
  STREAM_DESC,
168
  STREAM_IMAGE,
169 170 171 172 173 174 175 176 177 178
};

typedef void (*AsyncReadCbFunc) (gchar *data, gpointer user_data);

typedef struct {
  AsyncReadCbFunc callback;
  gchar *url;
  gpointer user_data;
} AsyncReadCb;

179 180 181
typedef struct {
  gchar *image;
  gchar *desc;
182
  gchar *published;
183 184
} PodcastData;

185 186 187 188 189 190 191 192
typedef struct {
  gchar *id;
  gchar *url;
  gchar *title;
  gchar *published;
  gchar *duration;
  gchar *summary;
  gchar *mime;
193
  gchar *image;
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
194
} Entry;
195

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
196
struct _GrlPodcastsPrivate {
197
  sqlite3 *db;
198
  GrlNetWc *wc;
199
  gboolean notify_changes;
200
  gint cache_time;
201 202
};

203
typedef struct {
204
  GrlSource *source;
205
  guint operation_id;
206
  const gchar *media_id;
207 208 209
  guint skip;
  guint count;
  const gchar *text;
210
  GrlSourceResultCb callback;
211 212
  guint error_code;
  gboolean is_query;
213
  time_t last_refreshed;
214
  gpointer user_data;
215
} OperationSpec;
216

217 218 219
typedef struct {
  OperationSpec *os;
  xmlDocPtr doc;
220 221 222 223 224
  xmlXPathContextPtr xpathCtx;
  xmlXPathObjectPtr xpathObj;
  guint parse_count;
  guint parse_index;
  guint parse_valid_index;
225
  GrlMedia *last_media;
226 227
} OperationSpecParse;

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
228
static GrlPodcastsSource *grl_podcasts_source_new (void);
229

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
230
static void grl_podcasts_source_finalize (GObject *plugin);
231

232
static const GList *grl_podcasts_source_supported_keys (GrlSource *source);
233

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
static void grl_podcasts_source_browse (GrlSource *source,
                                        GrlSourceBrowseSpec *bs);

static void grl_podcasts_source_search (GrlSource *source,
                                        GrlSourceSearchSpec *ss);

static void grl_podcasts_source_query (GrlSource *source,
                                       GrlSourceQuerySpec *qs);

static void grl_podcasts_source_resolve (GrlSource *source,
                                         GrlSourceResolveSpec *rs);

static void grl_podcasts_source_store (GrlSource *source,
                                       GrlSourceStoreSpec *ss);

static void grl_podcasts_source_remove (GrlSource *source,
                                        GrlSourceRemoveSpec *rs);

static gboolean grl_podcasts_source_notify_change_start (GrlSource *source,
253
                                                         GError **error);
254 255

static gboolean grl_podcasts_source_notify_change_stop (GrlSource *source,
256
                                                        GError **error);
257

258 259 260
/* =================== Podcasts Plugin  =============== */

static gboolean
261
grl_podcasts_plugin_init (GrlRegistry *registry,
262
                          GrlPlugin *plugin,
263
                          GList *configs)
264
{
265 266 267 268
  GrlConfig *config;
  gint config_count;
  gint cache_time;

269
  GRL_LOG_DOMAIN_INIT (podcasts_log_domain, "podcasts");
270 271

  GRL_DEBUG ("podcasts_plugin_init");
272

273 274 275 276
  /* Initialize i18n */
  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
277
  GrlPodcastsSource *source = grl_podcasts_source_new ();
278
  g_object_add_weak_pointer (G_OBJECT (source), (gpointer *) &source);
279 280 281 282
  grl_registry_register_source (registry,
                                plugin,
                                GRL_SOURCE (source),
                                NULL);
283 284 285
  if (source == NULL)
    return TRUE;
  g_object_remove_weak_pointer (G_OBJECT (source), (gpointer *) &source);
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

  source->priv->cache_time = DEFAULT_CACHE_TIME;
  if (!configs || !configs->data) {
    return TRUE;
  }

  config_count = g_list_length (configs);
  if (config_count > 1) {
    GRL_INFO ("Provided %d configs, but will only use one", config_count);
  }

  config = GRL_CONFIG (configs->data);

  cache_time = grl_config_get_int (config, "cache-time");
  if (cache_time <= 0) {
    /* Disable cache */
    source->priv->cache_time = 0;
    GRL_INFO ("Disabling cache");
  } else {
    /* Cache time in seconds */
    source->priv->cache_time = cache_time;
    GRL_INFO ("Setting cache time to %d seconds", cache_time);
  }

310 311 312
  return TRUE;
}

313 314 315 316 317 318 319 320 321 322 323 324
GRL_PLUGIN_DEFINE (GRL_MAJOR,
                   GRL_MINOR,
                   PODCASTS_PLUGIN_ID,
                   "Podcasts",
                   "A plugin for browsing podcasts",
                   "Igalia S.L.",
                   VERSION,
                   "LGPL",
                   "http://www.igalia.com",
                   grl_podcasts_plugin_init,
                   NULL,
                   NULL);
325 326 327

/* ================== Podcasts GObject ================ */

328 329
G_DEFINE_TYPE_WITH_PRIVATE (GrlPodcastsSource, grl_podcasts_source, GRL_TYPE_SOURCE)

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
330 331
static GrlPodcastsSource *
grl_podcasts_source_new (void)
332
{
333 334 335 336
  const char *tags[] = {
    "net:internet",
    NULL
  };
337
  GRL_DEBUG ("grl_podcasts_source_new");
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
338
  return g_object_new (GRL_PODCASTS_SOURCE_TYPE,
339 340 341
		       "source-id", SOURCE_ID,
		       "source-name", SOURCE_NAME,
		       "source-desc", SOURCE_DESC,
342
		       "source-tags", tags,
343 344 345 346
		       NULL);
}

static void
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
347
grl_podcasts_source_class_init (GrlPodcastsSourceClass * klass)
348 349
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
350
  GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
351

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
352
  gobject_class->finalize = grl_podcasts_source_finalize;
353

354
  source_class->supported_keys = grl_podcasts_source_supported_keys;
355 356 357 358 359 360 361 362
  source_class->browse = grl_podcasts_source_browse;
  source_class->search = grl_podcasts_source_search;
  source_class->query = grl_podcasts_source_query;
  source_class->resolve = grl_podcasts_source_resolve;
  source_class->store = grl_podcasts_source_store;
  source_class->remove = grl_podcasts_source_remove;
  source_class->notify_change_start = grl_podcasts_source_notify_change_start;
  source_class->notify_change_stop = grl_podcasts_source_notify_change_stop;
363 364 365
}

static void
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
366
grl_podcasts_source_init (GrlPodcastsSource *source)
367 368
{
  gint r;
369
  gchar *path;
370 371 372
  gchar *db_path;
  gchar *sql_error = NULL;

373
  source->priv = grl_podcasts_source_get_instance_private (source);
374

375 376 377 378 379 380
  path = g_strconcat (g_get_user_data_dir (),
                      G_DIR_SEPARATOR_S, "grilo-plugins",
                      NULL);

  if (!g_file_test (path, G_FILE_TEST_IS_DIR)) {
    g_mkdir_with_parents (path, 0775);
381 382
  }

383
  GRL_DEBUG ("Opening database connection...");
384
  db_path = g_strconcat (path, G_DIR_SEPARATOR_S, GRL_SQL_DB, NULL);
385
  r = sqlite3_open (db_path, &source->priv->db);
386 387 388
  g_free (path);
  g_free (db_path);

389 390 391 392
  if (r) {
    g_critical ("Failed to open database '%s': %s",
		db_path, sqlite3_errmsg (source->priv->db));
    sqlite3_close (source->priv->db);
393
    source->priv->db = NULL;
394 395
    return;
  }
396
  GRL_DEBUG ("  OK");
397

398
  GRL_DEBUG ("Checking database tables...");
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
399
  r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_PODCASTS,
400
		    NULL, NULL, &sql_error);
401 402 403

  if (!r) {
    /* TODO: if this fails, sqlite stays in an unreliable state fix that */
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
404
    r = sqlite3_exec (source->priv->db, GRL_SQL_CREATE_TABLE_STREAMS,
405 406
		      NULL, NULL, &sql_error);
  }
407 408
  if (r) {
    if (sql_error) {
409
      GRL_WARNING ("Failed to create database tables: %s", sql_error);
410
      g_clear_pointer (&sql_error, sqlite3_free);
411
    } else {
412
      GRL_WARNING ("Failed to create database tables.");
413 414
    }
    sqlite3_close (source->priv->db);
415
    source->priv->db = NULL;
416 417
    return;
  }
418
  GRL_DEBUG ("  OK");
419 420 421
}

static void
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
422
grl_podcasts_source_finalize (GObject *object)
423
{
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
424
  GrlPodcastsSource *source;
425

426
  GRL_DEBUG ("grl_podcasts_source_finalize");
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
427 428

  source = GRL_PODCASTS_SOURCE (object);
429

430
  g_clear_object (&source->priv->wc);
431

432 433
  if (source->priv->db)
    sqlite3_close (source->priv->db);
434

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
435
  G_OBJECT_CLASS (grl_podcasts_source_parent_class)->finalize (object);
436 437 438 439
}

/* ======================= Utilities ==================== */

440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
static void
print_entry (Entry *entry)
{
  g_print ("Entry Information:\n");
  g_print ("            ID: %s\n", entry->id);
  g_print ("         Title: %s\n", entry->title);
  g_print ("          Date: %s\n", entry->published);
  g_print ("      Duration: %s\n", entry->duration);
  g_print ("       Summary: %s\n", entry->summary);
  g_print ("           URL: %s\n", entry->url);
  g_print ("          Mime: %s\n", entry->mime);
}

static void
free_entry (Entry *entry)
{
  g_free (entry->id);
  g_free (entry->title);
  g_free (entry->published);
  g_free (entry->summary);
  g_free (entry->url);
  g_free (entry->mime);
462
  g_slice_free (Entry, entry);
463 464
}

465 466 467 468 469
static void
free_podcast_data (PodcastData *data)
{
  g_free (data->image);
  g_free (data->desc);
470
  g_free (data->published);
471 472 473
  g_slice_free (PodcastData, data);
}

474 475 476 477 478 479
static void
read_done_cb (GObject *source_object,
              GAsyncResult *res,
              gpointer user_data)
{
  AsyncReadCb *arc = (AsyncReadCb *) user_data;
480
  GError *wc_error = NULL;
481 482
  gchar *content = NULL;

483
  GRL_DEBUG ("  Done");
484

485 486 487 488 489 490 491 492
  grl_net_wc_request_finish (GRL_NET_WC (source_object),
                         res,
                         &content,
                         NULL,
                         &wc_error);
  if (wc_error) {
    GRL_WARNING ("Failed to open '%s': %s", arc->url, wc_error->message);
    g_error_free (wc_error);
493 494 495 496
  } else {
    arc->callback (content, arc->user_data);
  }
  g_free (arc->url);
497
  g_slice_free (AsyncReadCb, arc);
498 499 500
}

static void
501 502
read_url_async (GrlPodcastsSource *source,
                const gchar *url,
503 504 505 506 507
                AsyncReadCbFunc callback,
                gpointer user_data)
{
  AsyncReadCb *arc;

508
  GRL_DEBUG ("Opening async '%s'", url);
509

510
  arc = g_slice_new0 (AsyncReadCb);
511 512 513
  arc->url = g_strdup (url);
  arc->callback = callback;
  arc->user_data = user_data;
514 515 516 517 518

  /* We would need a different Wc if we change of URL.
   * In this case, as we don't know the previous URL,
   * we ditch the Wc and create another. It's cheap.
   */
519
  g_clear_object (&source->priv->wc);
520 521
  source->priv->wc = grl_net_wc_new ();
  grl_net_wc_request_async (source->priv->wc, url, NULL, read_done_cb, arc);
522 523 524 525 526 527 528 529 530 531 532
}

static gint
duration_to_seconds (const gchar *str)
{
  gint seconds = 0;
  gchar **parts;
  gint i;
  guint multiplier = 1;

  if (!str || str[0] == '\0') {
533
    return 0;
534 535 536 537 538 539 540 541 542
  }

  parts = g_strsplit (str, ":", 3);

  /* Get last portion (seconds) */
  i = 0;
  while (parts[i]) i++;
  if (i == 0) {
    g_strfreev (parts);
543
    return 0;
544 545 546
  } else {
    i--;
  }
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
547

548 549 550 551 552
  do {
    seconds += atoi (parts[i]) * multiplier;
    multiplier *= 60;
    i--;
  } while (i >= 0);
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
553

554 555 556 557 558 559 560 561
  g_strfreev (parts);

  return seconds;
}

static gboolean
mime_is_video (const gchar *mime)
{
562
  return mime && g_str_has_prefix (mime, "video/");
563 564 565 566 567
}

static gboolean
mime_is_audio (const gchar *mime)
{
568
  return mime && g_str_has_prefix (mime, "audio/");
569 570
}

571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
static gchar *
get_site_from_url (const gchar *url)
{
  gchar *p;

  if (g_str_has_prefix (url, "file://")) {
    return NULL;
  }

  p = strstr (url, "://");
  if (!p) {
    return NULL;
  } else {
    p += 3;
  }

  while (*p != '/') p++;

  return g_strndup (url, p - url);
}

592 593
static GrlMedia *
build_media (GrlMedia *content,
594 595 596 597 598 599 600
	     gboolean is_podcast,
	     const gchar *id,
	     const gchar *title,
	     const gchar *url,
	     const gchar *desc,
	     const gchar *mime,
	     const gchar *date,
601
             const gchar *image,
602 603
	     guint duration,
	     guint childcount)
604
{
605
  GrlMedia *media = NULL;
606
  gchar *site;
607 608 609 610 611 612 613

  if (content) {
    media = content;
  }

  if (is_podcast) {
    if (!media) {
614
      media = grl_media_container_new ();
615 616
    }

617
    grl_media_set_id (media, id);
618
    if (desc)
619
      grl_media_set_description (media, desc);
620
    grl_media_set_childcount (media, childcount);
621 622 623
  } else {
    if (!media) {
      if (mime_is_audio (mime)) {
624
	media = grl_media_audio_new ();
625
      } else if (mime_is_video (mime)) {
626
	media = grl_media_video_new ();
627
      } else {
628
	media = grl_media_new ();
629 630 631
      }
    }

632
    grl_media_set_id (media, url);
633
    if (date) {
634 635 636 637 638 639 640 641
      guint64 epoch;
      epoch = totem_pl_parser_parse_date (date, FALSE);
      if (epoch != -1) {
        GDateTime *time;
        time = g_date_time_new_from_unix_utc (epoch);
        grl_media_set_publication_date (media, time);
        g_date_time_unref (time);
      }
642
    }
643
    if (desc)
644
      grl_media_set_description (media, desc);
645
    if (mime)
646
      grl_media_set_mime (media, mime);
647
    if (duration > 0) {
648
      grl_media_set_duration (media, duration);
649 650
    }
  }
651

652 653
  grl_media_set_title (media, title);
  grl_media_set_url (media, url);
654 655
  if (image)
    grl_media_add_thumbnail (media, image);
656 657 658

  site = get_site_from_url (url);
  if (site) {
659
    grl_media_set_site (media, site);
660 661
    g_free (site);
  }
662 663 664 665

  return media;
}

666
static GrlMedia *
667 668
build_media_from_entry (Entry *entry)
{
669
  GrlMedia *media;
670 671 672
  gint duration;

  duration = duration_to_seconds (entry->duration);
673 674 675
  media = build_media (NULL, FALSE,
		       entry->url, entry->title, entry->url,
		       entry->summary, entry->mime, entry->published,
676
		       entry->image, duration, 0);
677 678 679
  return media;
}

680 681
static GrlMedia *
build_media_from_stmt (GrlMedia *content,
682 683
		       sqlite3_stmt *sql_stmt,
		       gboolean is_podcast)
684
{
685
  GrlMedia *media;
686 687 688 689 690 691
  gchar *id;
  gchar *title;
  gchar *url;
  gchar *desc;
  gchar *mime;
  gchar *date;
692
  gchar *image;
693
  guint duration;
694
  guint childcount;
695 696 697 698 699 700

  if (is_podcast) {
    id = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_ID);
    title = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_TITLE);
    url = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_URL);
    desc = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_DESC);
701
    image = (gchar *) sqlite3_column_text (sql_stmt, PODCAST_IMAGE);
702
    childcount = (guint) sqlite3_column_int (sql_stmt, PODCAST_LAST);
703 704
    media = build_media (content, is_podcast, id,
                         title, url, desc, NULL, NULL, image, 0, childcount);
705
  } else {
706 707 708 709 710 711
    mime = (gchar *) sqlite3_column_text (sql_stmt, STREAM_MIME);
    url = (gchar *) sqlite3_column_text (sql_stmt, STREAM_URL);
    title = (gchar *) sqlite3_column_text (sql_stmt, STREAM_TITLE);
    date = (gchar *) sqlite3_column_text (sql_stmt, STREAM_DATE);
    desc = (gchar *) sqlite3_column_text (sql_stmt, STREAM_DESC);
    duration = sqlite3_column_int (sql_stmt, STREAM_LENGTH);
712
    image = (gchar *) sqlite3_column_text (sql_stmt, STREAM_IMAGE);
713
    media = build_media (content, is_podcast, url,
714
                         title, url, desc, mime, date, image, duration, 0);
715 716 717 718 719 720 721 722 723 724 725 726 727
  }

  return media;
}

static void
produce_podcast_contents_from_db (OperationSpec *os)
{
  sqlite3 *db;
  gchar *sql;
  sqlite3_stmt *sql_stmt = NULL;
  GList *iter, *medias = NULL;
  guint count = 0;
728
  GrlMedia *media;
729 730 731
  gint r;
  GError *error = NULL;

732
  GRL_DEBUG ("produce_podcast_contents_from_db");
733

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
734
  db = GRL_PODCASTS_SOURCE (os->source)->priv->db;
735 736
  /* Check if searching or browsing */
  if (os->is_query) {
737 738 739 740 741 742 743 744 745 746
    if (os->text) {
      /* Search text */
      sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS_BY_TEXT,
                             os->text, os->text, os->text, os->text,
                             os->count, os->skip);
    } else {
      /* Return all */
      sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS_ALL,
                             os->count, os->skip);
    }
747 748 749 750
  } else {
    sql = g_strdup_printf (GRL_SQL_GET_PODCAST_STREAMS,
                           os->media_id, os->count, os->skip);
  }
751
  GRL_DEBUG ("%s", sql);
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
752
  r = sqlite3_prepare_v2 (db, sql, strlen (sql), &sql_stmt, NULL);
753 754 755
  g_free (sql);

  if (r != SQLITE_OK) {
756
    GRL_WARNING ("Failed to retrieve podcast streams: %s", sqlite3_errmsg (db));
757
    error = g_error_new (GRL_CORE_ERROR,
758 759 760
                         os->error_code,
                         _("Failed to get podcast streams: %s"),
                         sqlite3_errmsg (db));
761 762 763 764
    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
    g_error_free (error);
    return;
  }
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
765

766 767 768 769 770 771 772 773 774 775
  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);

  while (r == SQLITE_ROW) {
    media = build_media_from_stmt (NULL, sql_stmt, FALSE);
    medias = g_list_prepend (medias, media);
    count++;
    r = sqlite3_step (sql_stmt);
  }

  if (r != SQLITE_DONE) {
Alberto Garcia's avatar
Alberto Garcia committed
776
    GRL_WARNING ("Failed to retrieve podcast streams: %s", sqlite3_errmsg (db));
777
    error = g_error_new (GRL_CORE_ERROR,
778 779 780
                         os->error_code,
                         _("Failed to get podcast streams: %s"),
                         sqlite3_errmsg (db));
781 782 783 784 785 786 787
    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
    g_error_free (error);
    sqlite3_finalize (sql_stmt);
    return;
  }

  sqlite3_finalize (sql_stmt);
788

789 790 791 792
  if (count > 0) {
    medias = g_list_reverse (medias);
    iter = medias;
    while (iter) {
793
      media = GRL_MEDIA (iter->data);
794 795 796 797 798 799 800 801 802
      os->callback (os->source,
		    os->operation_id,
		    media,
		    --count,
		    os->user_data,
		    NULL);
      iter = g_list_next (iter);
    }
    g_list_free (medias);
803
  } else {
804 805 806 807
    os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
  }
}

808 809 810 811 812 813 814
static void
remove_podcast_streams (sqlite3 *db, const gchar *podcast_id, GError **error)
{
  gchar *sql;
  gchar *sql_error;
  gint r;

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
815
  sql = g_strdup_printf (GRL_SQL_DELETE_PODCAST_STREAMS, podcast_id);
816
  GRL_DEBUG ("%s", sql);
817 818 819
  r = sqlite3_exec (db, sql, NULL, NULL, &sql_error);
  g_free (sql);
  if (r) {
820
    GRL_WARNING ("Failed to remove podcast streams cache: %s", sql_error);
821
    *error = g_error_new (GRL_CORE_ERROR,
822 823 824
                          GRL_CORE_ERROR_REMOVE_FAILED,
                          _("Failed to remove: %s"),
                          sql_error);
825 826 827 828 829
    sqlite3_free (error);
  }
}

static void
830 831 832
remove_podcast (GrlPodcastsSource *podcasts_source,
                const gchar *podcast_id,
                GError **error)
833 834 835 836 837
{
  gint r;
  gchar *sql_error;
  gchar *sql;

838
  GRL_DEBUG ("remove_podcast");
839

840
  remove_podcast_streams (podcasts_source->priv->db, podcast_id, error);
841 842 843 844
  if (*error) {
    return;
  }

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
845
  sql = g_strdup_printf (GRL_SQL_REMOVE_PODCAST, podcast_id);
846
  GRL_DEBUG ("%s", sql);
847
  r = sqlite3_exec (podcasts_source->priv->db, sql, NULL, NULL, &sql_error);
848 849 850
  g_free (sql);

  if (r != SQLITE_OK) {
851
    GRL_WARNING ("Failed to remove podcast '%s': %s", podcast_id, sql_error);
852 853 854 855 856
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_REMOVE_FAILED,
                 _("Failed to remove: %s"),
                 sql_error);
857
    sqlite3_free (sql_error);
858
  } else if (podcasts_source->priv->notify_changes) {
859 860 861 862
    grl_source_notify_change (GRL_SOURCE (podcasts_source),
                              NULL,
                              GRL_CONTENT_REMOVED,
                              TRUE);
863 864 865 866
  }
}

static void
867 868 869
remove_stream (GrlPodcastsSource *podcasts_source,
               const gchar *url,
               GError **error)
870 871 872 873 874
{
  gint r;
  gchar *sql_error;
  gchar *sql;

875
  GRL_DEBUG ("remove_stream");
876

Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
877
  sql = g_strdup_printf (GRL_SQL_REMOVE_STREAM, url);
878
  GRL_DEBUG ("%s", sql);
879
  r = sqlite3_exec (podcasts_source->priv->db, sql, NULL, NULL, &sql_error);
880 881 882
  g_free (sql);

  if (r != SQLITE_OK) {
883
    GRL_WARNING ("Failed to remove podcast stream '%s': %s", url, sql_error);
884 885 886 887 888
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_REMOVE_FAILED,
                 _("Failed to remove: %s"),
                 sql_error);
889
    sqlite3_free (sql_error);
890
  } else if (podcasts_source->priv->notify_changes) {
891 892 893 894
    grl_source_notify_change (GRL_SOURCE (podcasts_source),
                              NULL,
                              GRL_CONTENT_REMOVED,
                              TRUE);
895 896 897
  }
}

898
static void
899
store_podcast (GrlPodcastsSource *podcasts_source,
900
               GList **keylist,
901 902
               GrlMedia *podcast,
               GError **error)
903 904 905 906 907 908
{
  gint r;
  sqlite3_stmt *sql_stmt = NULL;
  const gchar *title;
  const gchar *url;
  const gchar *desc;
909
  gchar *id;
910

911
  GRL_DEBUG ("store_podcast");
912

913 914 915
  title = grl_media_get_title (podcast);
  url = grl_media_get_url (podcast);
  desc = grl_media_get_description (podcast);
916

917
  GRL_DEBUG ("%s", GRL_SQL_STORE_PODCAST);
918
  r = sqlite3_prepare_v2 (podcasts_source->priv->db,
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
919 920 921
			  GRL_SQL_STORE_PODCAST,
			  strlen (GRL_SQL_STORE_PODCAST),
			  &sql_stmt, NULL);
922
  if (r != SQLITE_OK) {
923
    GRL_WARNING ("Failed to store podcast '%s': %s", title,
924 925 926 927
                 sqlite3_errmsg (podcasts_source->priv->db));
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_STORE_FAILED,
928 929
                 _("Failed to store: %s"),
                 sqlite3_errmsg (podcasts_source->priv->db));
930 931 932 933
    return;
  }

  sqlite3_bind_text (sql_stmt, 1, url, -1, SQLITE_STATIC);
934 935 936 937 938 939 940 941 942 943 944
  *keylist = g_list_remove (*keylist,
                            GRLKEYID_TO_POINTER (GRL_METADATA_KEY_URL));

  if (title) {
    sqlite3_bind_text (sql_stmt, 2, title, -1, SQLITE_STATIC);
    *keylist = g_list_remove (*keylist,
                              GRLKEYID_TO_POINTER (GRL_METADATA_KEY_TITLE));
  } else {
    sqlite3_bind_text (sql_stmt, 2, url, -1, SQLITE_STATIC);
  }

945 946
  if (desc) {
    sqlite3_bind_text (sql_stmt, 3, desc, -1, SQLITE_STATIC);
947 948
    *keylist = g_list_remove (*keylist,
                              GRLKEYID_TO_POINTER (GRL_METADATA_KEY_DESCRIPTION));
949 950 951 952 953 954 955
  } else {
    sqlite3_bind_text (sql_stmt, 3, "", -1, SQLITE_STATIC);
  }

  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);

  if (r != SQLITE_DONE) {
956
    GRL_WARNING ("Failed to store podcast '%s': %s", title,
957 958 959 960
                 sqlite3_errmsg (podcasts_source->priv->db));
    g_set_error (error,
                 GRL_CORE_ERROR,
                 GRL_CORE_ERROR_STORE_FAILED,
961 962
                 _("Failed to store: %s"),
                 sqlite3_errmsg (podcasts_source->priv->db));
963 964 965 966 967
    sqlite3_finalize (sql_stmt);
    return;
  }

  sqlite3_finalize (sql_stmt);
968

969 970
  id = g_strdup_printf ("%llu",
                        sqlite3_last_insert_rowid (podcasts_source->priv->db));
971
  grl_media_set_id (podcast, id);
972
  g_free (id);
973 974

  if (podcasts_source->priv->notify_changes) {
975 976 977 978
    grl_source_notify_change (GRL_SOURCE (podcasts_source),
                              NULL,
                              GRL_CONTENT_ADDED,
                              FALSE);
979
  }
980 981
}

982 983 984 985 986 987 988
static void
store_stream (sqlite3 *db, const gchar *podcast_id, Entry *entry)
{
  gint r;
  guint seconds;
  sqlite3_stmt *sql_stmt = NULL;

989
  if (!entry->url || entry->url[0] == '\0') {
990
    GRL_DEBUG ("Podcast stream has no URL, skipping");
991 992 993
    return;
  }

994
  seconds = duration_to_seconds (entry->duration);
995
  GRL_DEBUG ("%s", GRL_SQL_STORE_STREAM);
996
  r = sqlite3_prepare_v2 (db,
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
997 998 999
			  GRL_SQL_STORE_STREAM,
			  strlen (GRL_SQL_STORE_STREAM),
			  &sql_stmt, NULL);
1000
  if (r != SQLITE_OK) {
1001 1002
    GRL_WARNING ("Failed to store podcast stream '%s': %s",
                 entry->url, sqlite3_errmsg (db));
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
    return;
  }

  sqlite3_bind_text (sql_stmt, 1, podcast_id, -1, SQLITE_STATIC);
  sqlite3_bind_text (sql_stmt, 2, entry->url, -1, SQLITE_STATIC);
  sqlite3_bind_text (sql_stmt, 3, entry->title, -1, SQLITE_STATIC);
  sqlite3_bind_int  (sql_stmt, 4, seconds);
  sqlite3_bind_text (sql_stmt, 5, entry->mime, -1, SQLITE_STATIC);
  sqlite3_bind_text (sql_stmt, 6, entry->published, -1, SQLITE_STATIC);
  sqlite3_bind_text (sql_stmt, 7, entry->summary, -1, SQLITE_STATIC);
1013
  sqlite3_bind_text (sql_stmt, 8, entry->image, -1, SQLITE_STATIC);
1014 1015 1016 1017

  while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);

  if (r != SQLITE_DONE) {
1018 1019
    GRL_WARNING ("Failed to store podcast stream '%s': %s",
                 entry->url, sqlite3_errmsg (db));
1020 1021 1022 1023 1024
  }

  sqlite3_finalize (sql_stmt);
}

1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
static PodcastData *
parse_podcast_data (xmlDocPtr doc, xmlXPathObjectPtr xpathObj)
{
  xmlNodeSetPtr nodes;
  xmlNodePtr node;
  PodcastData *podcast_data = NULL;

  nodes = xpathObj->nodesetval;
  if (!nodes || !nodes->nodeTab) {
    return NULL;
  }

  /* Loop through the podcast data (we skip the "item" tags, since
     the podcast entries will be parsed later on */

1040 1041
  /* At the moment we are only interested in
     'image', 'description' and 'pubDate' tags */
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056
  podcast_data = g_slice_new0 (PodcastData);
  node = nodes->nodeTab[0]->xmlChildrenNode;
  while (node && xmlStrcmp (node->name, (const xmlChar *) "item")) {
    if (!xmlStrcmp (node->name, (const xmlChar *) "image")) {
      xmlNodePtr imgNode = node->xmlChildrenNode;
      while (imgNode && xmlStrcmp (imgNode->name, (const xmlChar *) "url")) {
        imgNode = imgNode->next;
      }
      if (imgNode) {
        podcast_data->image =
          (gchar *) xmlNodeListGetString (doc, imgNode->xmlChildrenNode, 1);
      }
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "description")) {
      podcast_data->desc =
	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
1057 1058 1059
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "pubDate")) {
      podcast_data->published =
	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
1060 1061 1062 1063 1064 1065 1066
    }
    node = node->next;
  }

  return podcast_data;
}

1067 1068 1069 1070 1071 1072 1073
static void
parse_entry (xmlDocPtr doc, xmlNodePtr entry, Entry *data)
{
  xmlNodePtr node;
  node = entry->xmlChildrenNode;
  while (node) {
    if (!xmlStrcmp (node->name, (const xmlChar *) "title")) {
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
1074
      data->title =
1075 1076 1077 1078 1079 1080
	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "enclosure")) {
      data->id = (gchar *) xmlGetProp (node, (xmlChar *) "url");
      data->url = g_strdup (data->id);
      data->mime = (gchar *) xmlGetProp (node, (xmlChar *) "type");
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "summary")) {
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
1081
      data->summary =
1082 1083
	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "pubDate")) {
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
1084
      data->published =
1085 1086
	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "duration")) {
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
1087
      data->duration =
1088
	(gchar *) xmlNodeListGetString (doc, node->xmlChildrenNode, 1);
1089 1090 1091 1092 1093
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "image")) {
      if (!data->image) {
        data->image = (gchar *) xmlGetProp (node, (xmlChar *) "href");
      }
    } else if (!xmlStrcmp (node->name, (const xmlChar *) "thumbnail")) {
1094
      g_clear_pointer (&data->image, g_free);
1095
      data->image = (gchar *) xmlGetProp (node, (xmlChar *) "url");
1096 1097 1098 1099 1100 1101
    }
    node = node->next;
  }
}

static void
1102
touch_podcast (sqlite3 *db, const gchar *podcast_id, PodcastData *data)
1103 1104
{
  gint r;
1105
  sqlite3_stmt *sql_stmt = NULL;
1106 1107
  GTimeVal now;
  gchar *now_str;
1108 1109
  gchar *img;
  gchar *desc;
1110

1111
  GRL_DEBUG ("touch_podcast");
1112

1113 1114
  g_get_current_time (&now);
  now_str = g_time_val_to_iso8601 (&now);
1115 1116
  desc = data->desc ? data->desc : "";
  img = data->image ? data->image : "";
1117

1118 1119 1120 1121
  r = sqlite3_prepare_v2 (db,
			  GRL_SQL_TOUCH_PODCAST,
			  strlen (GRL_SQL_TOUCH_PODCAST),
			  &sql_stmt, NULL);
1122
  if (r != SQLITE_OK) {
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137
    GRL_WARNING ("Failed to touch podcast '%s': %s",
                 podcast_id, sqlite3_errmsg (db));
  } else {
    sqlite3_bind_text (sql_stmt, 1, now_str, -1, SQLITE_STATIC);
    sqlite3_bind_text (sql_stmt, 2, desc, -1, SQLITE_STATIC);
    sqlite3_bind_text (sql_stmt, 3, img, -1, SQLITE_STATIC);
    sqlite3_bind_text (sql_stmt, 4, podcast_id, -1, SQLITE_STATIC);

    while ((r = sqlite3_step (sql_stmt)) == SQLITE_BUSY);
    if (r != SQLITE_DONE) {
      GRL_WARNING ("Failed to touch podcast '%s': %s", podcast_id,
                   sqlite3_errmsg (db));
    }

    sqlite3_finalize (sql_stmt);
1138
  }
1139 1140

  g_free (now_str);
1141 1142
}

1143 1144 1145 1146
static gboolean
parse_entry_idle (gpointer user_data)
{
  OperationSpecParse *osp = (OperationSpecParse *) user_data;
1147 1148
  xmlNodeSetPtr nodes;
  guint remaining;
1149
  GrlMedia *media;
1150

1151
  nodes = osp->xpathObj->nodesetval;
1152

1153
  /* Parse entry */
1154
  Entry *entry = g_slice_new0 (Entry);
1155 1156 1157
  if (nodes->nodeTab) {
    parse_entry (osp->doc, nodes->nodeTab[osp->parse_index], entry);
  }
1158 1159 1160 1161
  if (0) print_entry (entry);

  /* Check if entry is valid */
  if (!entry->url || entry->url[0] == '\0') {
1162
    GRL_DEBUG ("Podcast stream has no URL, skipping");
1163 1164 1165 1166 1167 1168 1169 1170 1171 1172
  } else {
    /* Provide results to user as fast as possible */
    if (osp->parse_valid_index >= osp->os->skip &&
	osp->parse_valid_index < osp->os->skip + osp->os->count) {
      media = build_media_from_entry (entry);
      remaining = osp->os->skip + osp->os->count - osp->parse_valid_index - 1;

      /* Hack: if we emit the last result now the UI may request more results
	 right away while we are still parsing the XML, so we keep the last
	 result until we are done processing the whole feed, this way when
1173
	 the next query arrives all the stuff is stored in the database
1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189
	 and the query can be resolved normally */
      if (remaining > 0) {
	osp->os->callback (osp->os->source,
			   osp->os->operation_id,
			   media,
			   remaining,
			   osp->os->user_data,
			   NULL);
      } else {
	osp->last_media = media;
      }
    }

    osp->parse_valid_index++;

    /* And store stream in database cache */
1190 1191 1192 1193
    store_stream (GRL_PODCASTS_SOURCE (osp->os->source)->priv->db,
		  osp->os->media_id, entry);
  }

1194 1195
  osp->parse_index++;
  free_entry (entry);
1196

1197 1198 1199 1200 1201 1202 1203 1204
  if (osp->parse_index >= osp->parse_count) {
    /* Send last result */
    osp->os->callback (osp->os->source,
		       osp->os->operation_id,
		       osp->last_media,
		       0,
		       osp->os->user_data,
		       NULL);
1205 1206
    /* Notify about changes */
    if (GRL_PODCASTS_SOURCE (osp->os->source)->priv->notify_changes) {
1207
      media = grl_media_container_new ();
1208
      grl_media_set_id (media, osp->os->media_id);
1209 1210 1211 1212
      grl_source_notify_change (GRL_SOURCE (osp->os->source),
                                media,
                                GRL_CONTENT_CHANGED,
                                FALSE);
1213 1214
      g_object_unref (media);
    }
1215
    g_slice_free (OperationSpec, osp->os);
1216 1217
    xmlXPathFreeObject (osp->xpathObj);
    xmlXPathFreeContext (osp->xpathCtx);
1218
    xmlFreeDoc (osp->doc);
1219
    g_slice_free (OperationSpecParse, osp);
1220
  }
1221

1222
  return osp->parse_index < osp->parse_count;
1223 1224
}

1225 1226 1227
static void
parse_feed (OperationSpec *os, const gchar *str, GError **error)
{
1228
  GrlPodcastsSource *source;
1229
  GrlMedia *podcast = NULL;
1230 1231 1232 1233
  xmlDocPtr doc = NULL;
  xmlXPathContextPtr xpathCtx = NULL;
  xmlXPathObjectPtr xpathObj = NULL;
  guint stream_count;
1234
  PodcastData *podcast_data = NULL;
1235
  guint id;
1236

1237
  GRL_DEBUG ("parse_feed");
1238

1239 1240
  source = GRL_PODCASTS_SOURCE (os->source);

1241
  doc = xmlParseDoc ((xmlChar *) str);
1242
  if (!doc) {
1243 1244 1245
    *error = g_error_new_literal (GRL_CORE_ERROR,
                                  os->error_code,
                                  _("Failed to parse content"));
1246 1247 1248
    goto free_resources;
  }

1249 1250 1251
  /* Get feed stream list */
  xpathCtx = xmlXPathNewContext (doc);
  if (!xpathCtx) {
1252 1253 1254
    *error = g_error_new_literal (GRL_CORE_ERROR,
                                  os->error_code,
                                  _("Failed to parse content"));
1255
    goto free_resources;
1256
  }
1257 1258 1259 1260 1261

  /* Check podcast data */
  xpathObj = xmlXPathEvalExpression ((xmlChar *) "/rss/channel",
				     xpathCtx);
  if(xpathObj == NULL) {
1262 1263 1264
    *error = g_error_new_literal (GRL_CORE_ERROR,
                                  os->error_code,
                                  _("Failed to parse content"));
1265 1266
    goto free_resources;
  }
1267

1268 1269
  podcast_data = parse_podcast_data (doc, xpathObj);
  xmlXPathFreeObject (xpathObj);
1270 1271
  xpathObj = NULL;

1272 1273 1274 1275 1276 1277 1278
  if(podcast_data == NULL) {
    *error = g_error_new_literal (GRL_CORE_ERROR,
                                  os->error_code,
                                  _("Failed to parse podcast contents"));
    goto free_resources;
  }

1279 1280 1281
  /* Check podcast pubDate (if available), if it has not been updated
     recently then we can use the cache and avoid parsing the feed */
  if (podcast_data->published != NULL) {
1282 1283 1284
    guint64 pub_time;
    pub_time = totem_pl_parser_parse_date (podcast_data->published, FALSE);
    if (pub_time != -1) {
1285 1286 1287 1288 1289 1290 1291 1292 1293 1294
      GRL_DEBUG ("Invalid podcast pubDate: '%s'", podcast_data->published);
      /* We will parse the feed again just in case */
    } else if (os->last_refreshed >= pub_time) {
      GRL_DEBUG ("Podcast feed is up-to-date");
      /* We do not need to parse again, we already have the contents in cache */
      produce_podcast_contents_from_db (os);
      g_slice_free (OperationSpec, os);
      goto free_resources;
    }
  }
1295

1296 1297
  /* The podcast has been updated since the last time
     we processed it, we have to parse it again */
1298

1299 1300 1301
  xpathObj = xmlXPathEvalExpression ((xmlChar *) "/rss/channel/item",
				     xpathCtx);
  if(xpathObj == NULL) {
1302 1303 1304
    *error = g_error_new_literal (GRL_CORE_ERROR,
                                  os->error_code,
                                  _("Failed to parse podcast contents"));
1305 1306
    goto free_resources;
  }
Juan A. Suárez Romero's avatar
Juan A. Suárez Romero committed
1307

1308
  /* Feed is ok, let's process it */
1309

1310
  /* First, remove old entries for this podcast */
1311
  remove_podcast_streams (source->priv->db,  os->media_id, error);
1312 1313 1314
  if (*error) {
    (*error)->code = os->error_code;
    goto free_resources;
1315 1316
  }

1317
  /* Then update the podcast data, including the last_refreshed date */
1318
  touch_podcast (source->priv->db, os