No issues found
Tool | Failure ID | Location | Function | Message | Data |
---|---|---|---|---|---|
clang-analyzer | no-output-found | rb-podcast-manager.c | Message(text='Unable to locate XML output from invoke-clang-analyzer') | None | |
clang-analyzer | no-output-found | rb-podcast-manager.c | Message(text='Unable to locate XML output from invoke-clang-analyzer') | None |
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2005 Renato Araujo Oliveira Filho - INdT <renato.filho@indt.org.br>
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 #define __USE_XOPEN
33 #include <time.h>
34
35 #include <glib/gi18n.h>
36 #include <glib/gstdio.h>
37 #include <gio/gio.h>
38 #include <gtk/gtk.h>
39
40 #include "rb-podcast-settings.h"
41 #include "rb-podcast-manager.h"
42 #include "rb-podcast-entry-types.h"
43 #include "rb-podcast-search.h"
44 #include "rb-file-helpers.h"
45 #include "rb-debug.h"
46 #include "rb-marshal.h"
47 #include "rhythmdb.h"
48 #include "rhythmdb-query-model.h"
49 #include "rb-podcast-parse.h"
50 #include "rb-dialog.h"
51 #include "rb-metadata.h"
52 #include "rb-util.h"
53 #include "rb-missing-plugins.h"
54 #include "rb-ext-db.h"
55
56 enum
57 {
58 PROP_0,
59 PROP_DB
60 };
61
62 enum
63 {
64 START_DOWNLOAD,
65 FINISH_DOWNLOAD,
66 PROCESS_ERROR,
67 FEED_UPDATES_AVAILABLE,
68 LAST_SIGNAL
69 };
70
71 /* passed from feed parsing threads back to main thread */
72 typedef struct
73 {
74 GError *error;
75 RBPodcastChannel *channel;
76 RBPodcastManager *pd;
77 gboolean automatic;
78 } RBPodcastManagerParseResult;
79
80 typedef struct
81 {
82 RBPodcastManager *pd;
83
84 RhythmDBEntry *entry;
85 char *query_string;
86
87 GFile *source;
88 GFile *destination;
89 GFileInputStream *in_stream;
90 GFileOutputStream *out_stream;
91
92 guint64 download_offset;
93 guint64 download_size;
94 guint progress;
95
96 GCancellable *cancel;
97 GThread *thread;
98 } RBPodcastManagerInfo;
99
100 typedef struct
101 {
102 RBPodcastManager *pd;
103 char *url;
104 gboolean automatic;
105 gboolean existing_feed;
106 } RBPodcastThreadInfo;
107
108 struct RBPodcastManagerPrivate
109 {
110 RhythmDB *db;
111 GList *download_list;
112 RBPodcastManagerInfo *active_download;
113 guint source_sync;
114 guint next_file_id;
115 gboolean shutdown;
116 RBExtDB *art_store;
117
118 GList *searches;
119 GSettings *settings;
120 GFile *timestamp_file;
121 };
122
123 #define RB_PODCAST_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_PODCAST_MANAGER, RBPodcastManagerPrivate))
124
125
126 static guint rb_podcast_manager_signals[LAST_SIGNAL] = { 0 };
127
128 /* functions */
129 static void rb_podcast_manager_class_init (RBPodcastManagerClass *klass);
130 static void rb_podcast_manager_init (RBPodcastManager *dp);
131 static void rb_podcast_manager_constructed (GObject *object);
132 static void rb_podcast_manager_dispose (GObject *object);
133 static void rb_podcast_manager_finalize (GObject *object);
134 static void rb_podcast_manager_set_property (GObject *object,
135 guint prop_id,
136 const GValue *value,
137 GParamSpec *pspec);
138 static void rb_podcast_manager_get_property (GObject *object,
139 guint prop_id,
140 GValue *value,
141 GParamSpec *pspec);
142 static void read_file_cb (GFile *source,
143 GAsyncResult *result,
144 RBPodcastManagerInfo *data);
145 static void download_file_info_cb (GFile *source,
146 GAsyncResult *result,
147 RBPodcastManagerInfo *data);
148 static void download_podcast (GFileInfo *src_info,
149 RBPodcastManagerInfo *data);
150 static void rb_podcast_manager_abort_download (RBPodcastManagerInfo *data);
151 static gboolean rb_podcast_manager_update_feeds_cb (gpointer data);
152 static void rb_podcast_manager_save_metadata (RBPodcastManager *pd,
153 RhythmDBEntry *entry);
154 static void rb_podcast_manager_db_entry_added_cb (RBPodcastManager *pd,
155 RhythmDBEntry *entry);
156 static gboolean rb_podcast_manager_next_file (RBPodcastManager * pd);
157 static void rb_podcast_manager_handle_feed_error (RBPodcastManager *mgr,
158 const char *url,
159 GError *error,
160 gboolean emit);
161
162 static gpointer rb_podcast_manager_thread_parse_feed (RBPodcastThreadInfo *info);
163 static void podcast_settings_changed_cb (GSettings *settings,
164 const char *key,
165 RBPodcastManager *mgr);
166
167 /* internal functions */
168 static void download_info_free (RBPodcastManagerInfo *data);
169 static gpointer podcast_download_thread (RBPodcastManagerInfo *data);
170 static gboolean end_job (RBPodcastManagerInfo *data);
171 static void cancel_job (RBPodcastManagerInfo *pd);
172 static void rb_podcast_manager_start_update_timer (RBPodcastManager *pd);
173
174 G_DEFINE_TYPE (RBPodcastManager, rb_podcast_manager, G_TYPE_OBJECT)
175
176 static void
177 rb_podcast_manager_class_init (RBPodcastManagerClass *klass)
178 {
179 GObjectClass *object_class = G_OBJECT_CLASS (klass);
180
181 object_class->constructed = rb_podcast_manager_constructed;
182 object_class->dispose = rb_podcast_manager_dispose;
183 object_class->finalize = rb_podcast_manager_finalize;
184
185 object_class->set_property = rb_podcast_manager_set_property;
186 object_class->get_property = rb_podcast_manager_get_property;
187
188 g_object_class_install_property (object_class,
189 PROP_DB,
190 g_param_spec_object ("db",
191 "db",
192 "database",
193 RHYTHMDB_TYPE,
194 G_PARAM_READWRITE));
195
196 rb_podcast_manager_signals[START_DOWNLOAD] =
197 g_signal_new ("start_download",
198 G_OBJECT_CLASS_TYPE (object_class),
199 G_SIGNAL_RUN_LAST,
200 G_STRUCT_OFFSET (RBPodcastManagerClass, start_download),
201 NULL, NULL,
202 g_cclosure_marshal_VOID__BOXED,
203 G_TYPE_NONE,
204 1,
205 RHYTHMDB_TYPE_ENTRY);
206
207 rb_podcast_manager_signals[FINISH_DOWNLOAD] =
208 g_signal_new ("finish_download",
209 G_OBJECT_CLASS_TYPE (object_class),
210 G_SIGNAL_RUN_LAST,
211 G_STRUCT_OFFSET (RBPodcastManagerClass, finish_download),
212 NULL, NULL,
213 g_cclosure_marshal_VOID__BOXED,
214 G_TYPE_NONE,
215 1,
216 RHYTHMDB_TYPE_ENTRY);
217
218 rb_podcast_manager_signals[FEED_UPDATES_AVAILABLE] =
219 g_signal_new ("feed_updates_available",
220 G_OBJECT_CLASS_TYPE (object_class),
221 G_SIGNAL_RUN_LAST,
222 G_STRUCT_OFFSET (RBPodcastManagerClass, feed_updates_available),
223 NULL, NULL,
224 g_cclosure_marshal_VOID__BOXED,
225 G_TYPE_NONE,
226 1,
227 RHYTHMDB_TYPE_ENTRY);
228
229 rb_podcast_manager_signals[PROCESS_ERROR] =
230 g_signal_new ("process_error",
231 G_OBJECT_CLASS_TYPE (object_class),
232 G_SIGNAL_RUN_LAST,
233 G_STRUCT_OFFSET (RBPodcastManagerClass, process_error),
234 NULL, NULL,
235 NULL,
236 G_TYPE_NONE,
237 3,
238 G_TYPE_STRING,
239 G_TYPE_STRING,
240 G_TYPE_BOOLEAN);
241
242 g_type_class_add_private (klass, sizeof (RBPodcastManagerPrivate));
243 }
244
245 static void
246 rb_podcast_manager_init (RBPodcastManager *pd)
247 {
248 pd->priv = RB_PODCAST_MANAGER_GET_PRIVATE (pd);
249
250 pd->priv->source_sync = 0;
251 pd->priv->db = NULL;
252
253 }
254
255 static void
256 rb_podcast_manager_constructed (GObject *object)
257 {
258 RBPodcastManager *pd = RB_PODCAST_MANAGER (object);
259 GFileOutputStream *st;
260 char *ts_file_path;
261
262 RB_CHAIN_GOBJECT_METHOD (rb_podcast_manager_parent_class, constructed, object);
263
264 /* add built in search types */
265 rb_podcast_manager_add_search (pd, rb_podcast_search_itunes_get_type ());
266 rb_podcast_manager_add_search (pd, rb_podcast_search_miroguide_get_type ());
267
268 pd->priv->settings = g_settings_new (PODCAST_SETTINGS_SCHEMA);
269 g_signal_connect_object (pd->priv->settings,
270 "changed",
271 G_CALLBACK (podcast_settings_changed_cb),
272 pd, 0);
273
274 ts_file_path = g_build_filename (rb_user_data_dir (), "podcast-timestamp", NULL);
275 pd->priv->timestamp_file = g_file_new_for_path (ts_file_path);
276 g_free (ts_file_path);
277
278 /* create it if it doesn't exist */
279 st = g_file_create (pd->priv->timestamp_file, G_FILE_CREATE_NONE, NULL, NULL);
280 if (st != NULL) {
281 g_output_stream_close (G_OUTPUT_STREAM (st), NULL, NULL);
282 g_object_unref (st);
283 }
284
285 pd->priv->art_store = rb_ext_db_new ("album-art");
286
287 rb_podcast_manager_start_update_timer (pd);
288 }
289
290 static void
291 rb_podcast_manager_dispose (GObject *object)
292 {
293 RBPodcastManager *pd;
294 g_return_if_fail (object != NULL);
295 g_return_if_fail (RB_IS_PODCAST_MANAGER (object));
296
297 pd = RB_PODCAST_MANAGER (object);
298 g_return_if_fail (pd->priv != NULL);
299
300 if (pd->priv->next_file_id != 0) {
301 g_source_remove (pd->priv->next_file_id);
302 pd->priv->next_file_id = 0;
303 }
304
305 if (pd->priv->source_sync != 0) {
306 g_source_remove (pd->priv->source_sync);
307 pd->priv->source_sync = 0;
308 }
309
310 if (pd->priv->db != NULL) {
311 g_object_unref (pd->priv->db);
312 pd->priv->db = NULL;
313 }
314
315 if (pd->priv->settings != NULL) {
316 g_object_unref (pd->priv->settings);
317 pd->priv->settings = NULL;
318 }
319
320 if (pd->priv->timestamp_file != NULL) {
321 g_object_unref (pd->priv->timestamp_file);
322 pd->priv->timestamp_file = NULL;
323 }
324
325 if (pd->priv->art_store != NULL) {
326 g_object_unref (pd->priv->art_store);
327 pd->priv->art_store = NULL;
328 }
329
330 G_OBJECT_CLASS (rb_podcast_manager_parent_class)->dispose (object);
331 }
332
333 static void
334 rb_podcast_manager_finalize (GObject *object)
335 {
336 RBPodcastManager *pd;
337 g_return_if_fail (object != NULL);
338 g_return_if_fail (RB_IS_PODCAST_MANAGER (object));
339
340 pd = RB_PODCAST_MANAGER(object);
341
342 g_return_if_fail (pd->priv != NULL);
343
344 if (pd->priv->download_list) {
345 g_list_foreach (pd->priv->download_list, (GFunc)g_free, NULL);
346 g_list_free (pd->priv->download_list);
347 }
348
349 g_list_free (pd->priv->searches);
350
351 G_OBJECT_CLASS (rb_podcast_manager_parent_class)->finalize (object);
352 }
353
354 static void
355 rb_podcast_manager_set_property (GObject *object,
356 guint prop_id,
357 const GValue *value,
358 GParamSpec *pspec)
359 {
360 RBPodcastManager *pd = RB_PODCAST_MANAGER (object);
361
362 switch (prop_id) {
363 case PROP_DB:
364 if (pd->priv->db) {
365 g_signal_handlers_disconnect_by_func (pd->priv->db,
366 G_CALLBACK (rb_podcast_manager_db_entry_added_cb),
367 pd);
368 g_object_unref (pd->priv->db);
369 }
370
371 pd->priv->db = g_value_get_object (value);
372 g_object_ref (pd->priv->db);
373
374 g_signal_connect_object (pd->priv->db,
375 "entry-added",
376 G_CALLBACK (rb_podcast_manager_db_entry_added_cb),
377 pd, G_CONNECT_SWAPPED);
378 break;
379 default:
380 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
381 }
382 }
383
384 static void
385 rb_podcast_manager_get_property (GObject *object,
386 guint prop_id,
387 GValue *value,
388 GParamSpec *pspec)
389 {
390 RBPodcastManager *pd = RB_PODCAST_MANAGER (object);
391
392 switch (prop_id) {
393 case PROP_DB:
394 g_value_set_object (value, pd->priv->db);
395 break;
396 default:
397 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
398 }
399
400 }
401
402 RBPodcastManager *
403 rb_podcast_manager_new (RhythmDB *db)
404 {
405 RBPodcastManager *pd;
406
407 pd = g_object_new (RB_TYPE_PODCAST_MANAGER, "db", db, NULL);
408 return pd;
409 }
410
411 static const char *
412 get_download_location (RhythmDBEntry *entry)
413 {
414 /* We haven't tried to download the entry yet */
415 if (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MOUNTPOINT) == NULL)
416 return NULL;
417 return rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
418 }
419
420 static void
421 set_download_location (RhythmDB *db, RhythmDBEntry *entry, GValue *value)
422 {
423 char *remote_location;
424
425 if (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MOUNTPOINT) == NULL) {
426 /* If the download location was never set */
427 GValue val = {0, };
428
429 /* Save the remote location */
430 remote_location = g_strdup (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION));
431 /* Set the new download location */
432 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_LOCATION, value);
433 /* Set MOUNTPOINT to the remote location */
434 g_value_init (&val, G_TYPE_STRING);
435 g_value_take_string (&val, remote_location);
436 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_MOUNTPOINT, &val);
437 g_value_unset (&val);
438 } else {
439 /* Just update the location */
440 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_LOCATION, value);
441 }
442 }
443
444 static const char *
445 get_remote_location (RhythmDBEntry *entry)
446 {
447 const char *location;
448 location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MOUNTPOINT);
449 if (location == NULL)
450 location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
451 return location;
452 }
453
454 void
455 rb_podcast_manager_download_entry (RBPodcastManager *pd,
456 RhythmDBEntry *entry)
457 {
458 gulong status;
459 g_assert (rb_is_main_thread ());
460
461 g_return_if_fail (RB_IS_PODCAST_MANAGER (pd));
462
463 if (entry == NULL)
464 return;
465
466 status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
467 if ((status < RHYTHMDB_PODCAST_STATUS_COMPLETE) ||
468 (status == RHYTHMDB_PODCAST_STATUS_WAITING)) {
469 RBPodcastManagerInfo *data;
470 GValue val = { 0, };
471 GTimeVal now;
472
473 if (status < RHYTHMDB_PODCAST_STATUS_COMPLETE) {
474 g_value_init (&val, G_TYPE_ULONG);
475 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_WAITING);
476 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
477 g_value_unset (&val);
478 }
479
480 /* set last seen time so it shows up in the 'new downloads' subsource */
481 g_value_init (&val, G_TYPE_ULONG);
482 g_get_current_time (&now);
483 g_value_set_ulong (&val, now.tv_sec);
484 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_LAST_SEEN, &val);
485 g_value_unset (&val);
486 rhythmdb_commit (pd->priv->db);
487
488 rb_debug ("Adding podcast episode %s to download list", get_remote_location (entry));
489
490 data = g_new0 (RBPodcastManagerInfo, 1);
491 data->pd = g_object_ref (pd);
492 data->entry = rhythmdb_entry_ref (entry);
493
494 pd->priv->download_list = g_list_append (pd->priv->download_list, data);
495 if (pd->priv->next_file_id == 0) {
496 pd->priv->next_file_id =
497 g_idle_add ((GSourceFunc) rb_podcast_manager_next_file, pd);
498 }
499 }
500 }
501
502 gboolean
503 rb_podcast_manager_entry_downloaded (RhythmDBEntry *entry)
504 {
505 gulong status;
506 const gchar *file_name;
507 RhythmDBEntryType *type = rhythmdb_entry_get_entry_type (entry);
508
509 g_assert (type == RHYTHMDB_ENTRY_TYPE_PODCAST_POST);
510
511 status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
512 file_name = get_download_location (entry);
513
514 return (status != RHYTHMDB_PODCAST_STATUS_ERROR && file_name != NULL);
515 }
516
517 gboolean
518 rb_podcast_manager_entry_in_download_queue (RBPodcastManager *pd, RhythmDBEntry *entry)
519 {
520 RBPodcastManagerInfo *info;
521 GList *l;
522
523 for (l = pd->priv->download_list; l != NULL; l = l->next) {
524 info = l->data;
525 if (info->entry == entry) {
526 return TRUE;
527 }
528 }
529
530 return FALSE;
531 }
532
533 static void
534 rb_podcast_manager_start_update_timer (RBPodcastManager *pd)
535 {
536 guint64 last_time;
537 guint64 interval_sec;
538 guint64 now;
539 GFileInfo *fi;
540 RBPodcastInterval interval;
541
542 g_return_if_fail (RB_IS_PODCAST_MANAGER (pd));
543
544 if (pd->priv->source_sync != 0) {
545 g_source_remove (pd->priv->source_sync);
546 pd->priv->source_sync = 0;
547 }
548
549 interval = g_settings_get_enum (pd->priv->settings,
550 PODCAST_DOWNLOAD_INTERVAL);
551 if (interval == PODCAST_INTERVAL_MANUAL) {
552 rb_debug ("periodic podcast updates disabled");
553 return;
554 }
555
556 /* get last update time */
557 fi = g_file_query_info (pd->priv->timestamp_file,
558 G_FILE_ATTRIBUTE_TIME_MODIFIED,
559 G_FILE_QUERY_INFO_NONE,
560 NULL,
561 NULL);
562 if (fi != NULL) {
563 last_time = g_file_info_get_attribute_uint64 (fi, G_FILE_ATTRIBUTE_TIME_MODIFIED);
564 } else {
565 last_time = 0;
566 }
567
568 switch (interval) {
569 case PODCAST_INTERVAL_HOURLY:
570 interval_sec = 3600;
571 break;
572 case PODCAST_INTERVAL_DAILY:
573 interval_sec = (3600 * 24);
574 break;
575 case PODCAST_INTERVAL_WEEKLY:
576 interval_sec = (3600 * 24 * 7);
577 break;
578 default:
579 g_assert_not_reached ();
580 return;
581 }
582
583 /* wait until next update time */
584 now = time (NULL);
585 rb_debug ("last periodic update at %" G_GUINT64_FORMAT ", interval %" G_GUINT64_FORMAT ", time is now %" G_GUINT64_FORMAT,
586 last_time, interval_sec, now);
587
588 if (last_time + interval_sec < now) {
589 rb_debug ("periodic update should already have happened");
590 pd->priv->source_sync = g_idle_add ((GSourceFunc) rb_podcast_manager_update_feeds_cb,
591 pd);
592 } else {
593 rb_debug ("next periodic update in %" G_GUINT64_FORMAT " seconds", (last_time + interval_sec) - now);
594 pd->priv->source_sync = g_timeout_add_seconds ((last_time + interval_sec) - now,
595 (GSourceFunc) rb_podcast_manager_update_feeds_cb,
596 pd);
597 }
598 }
599
600 static gboolean
601 rb_podcast_manager_head_query_cb (GtkTreeModel *query_model,
602 GtkTreePath *path,
603 GtkTreeIter *iter,
604 RBPodcastManager *manager)
605 {
606 const char *uri;
607 RhythmDBEntry *entry;
608 guint status;
609
610 gtk_tree_model_get (query_model, iter, 0, &entry, -1);
611 uri = get_remote_location (entry);
612 status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
613
614 if (status == 1)
615 rb_podcast_manager_subscribe_feed (manager, uri, TRUE);
616
617 rhythmdb_entry_unref (entry);
618
619 return FALSE;
620 }
621
622 void
623 rb_podcast_manager_update_feeds (RBPodcastManager *pd)
624 {
625 GtkTreeModel *query_model;
626
627 query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (pd->priv->db));
628
629 rhythmdb_do_full_query (pd->priv->db,
630 RHYTHMDB_QUERY_RESULTS (query_model),
631 RHYTHMDB_QUERY_PROP_EQUALS,
632 RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
633 RHYTHMDB_QUERY_END);
634
635 gtk_tree_model_foreach (query_model,
636 (GtkTreeModelForeachFunc) rb_podcast_manager_head_query_cb,
637 pd);
638
639 g_object_unref (query_model);
640 }
641
642 static gboolean
643 rb_podcast_manager_update_feeds_cb (gpointer data)
644 {
645 RBPodcastManager *pd = RB_PODCAST_MANAGER (data);
646
647 g_assert (rb_is_main_thread ());
648
649 GDK_THREADS_ENTER ();
650
651 pd->priv->source_sync = 0;
652
653 g_file_set_attribute_uint64 (pd->priv->timestamp_file,
654 G_FILE_ATTRIBUTE_TIME_MODIFIED,
655 (guint64) time (NULL),
656 G_FILE_QUERY_INFO_NONE,
657 NULL,
658 NULL);
659
660 rb_podcast_manager_update_feeds (pd);
661
662 rb_podcast_manager_start_update_timer (pd);
663
664 GDK_THREADS_LEAVE ();
665 return FALSE;
666 }
667
668 static void
669 download_error (RBPodcastManagerInfo *data, GError *error)
670 {
671 GValue val = {0,};
672
673 if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) == FALSE) {
674 rb_debug ("error downloading %s: %s",
675 get_remote_location (data->entry),
676 error->message);
677
678 g_value_init (&val, G_TYPE_ULONG);
679 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_ERROR);
680 rhythmdb_entry_set (data->pd->priv->db, data->entry, RHYTHMDB_PROP_STATUS, &val);
681 g_value_unset (&val);
682
683 g_value_init (&val, G_TYPE_STRING);
684 g_value_set_string (&val, error->message);
685 rhythmdb_entry_set (data->pd->priv->db, data->entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &val);
686 g_value_unset (&val);
687 } else {
688 rb_debug ("download of %s was cancelled", get_remote_location (data->entry));
689 }
690
691 rhythmdb_commit (data->pd->priv->db);
692
693 if (rb_is_main_thread() == FALSE) {
694 g_idle_add ((GSourceFunc)end_job, data);
695 } else {
696 rb_podcast_manager_abort_download (data);
697 }
698 }
699
700 static gboolean
701 rb_podcast_manager_next_file (RBPodcastManager * pd)
702 {
703 const char *location;
704 RBPodcastManagerInfo *data;
705 char *query_string;
706 GList *d;
707
708 g_assert (rb_is_main_thread ());
709
710 rb_debug ("looking for something to download");
711
712 GDK_THREADS_ENTER ();
713
714 pd->priv->next_file_id = 0;
715
716 if (pd->priv->active_download != NULL) {
717 rb_debug ("already downloading something");
718 GDK_THREADS_LEAVE ();
719 return FALSE;
720 }
721
722 d = g_list_first (pd->priv->download_list);
723 if (d == NULL) {
724 rb_debug ("download queue is empty");
725 GDK_THREADS_LEAVE ();
726 return FALSE;
727 }
728
729 data = (RBPodcastManagerInfo *) d->data;
730 g_assert (data != NULL);
731 g_assert (data->entry != NULL);
732
733 pd->priv->active_download = data;
734
735 location = get_remote_location (data->entry);
736 rb_debug ("processing %s", location);
737
738 /* extract the query string so we can remove it later if it appears
739 * in download URLs
740 */
741 query_string = strchr (location, '?');
742 if (query_string != NULL) {
743 query_string--;
744 data->query_string = g_strdup (query_string);
745 }
746
747 data->source = g_file_new_for_uri (location);
748
749 g_file_read_async (data->source,
750 0,
751 data->cancel,
752 (GAsyncReadyCallback) read_file_cb,
753 data);
754
755 GDK_THREADS_LEAVE ();
756 return FALSE;
757 }
758
759 static void
760 read_file_cb (GFile *source,
761 GAsyncResult *result,
762 RBPodcastManagerInfo *data)
763 {
764 GError *error = NULL;
765 GFileInfo *src_info;
766
767 g_assert (rb_is_main_thread ());
768
769 rb_debug ("started read for %s",
770 get_remote_location (data->entry));
771
772 data->in_stream = g_file_read_finish (data->source,
773 result,
774 &error);
775 if (error != NULL) {
776 download_error (data, error);
777 g_error_free (error);
778 return;
779 }
780
781 src_info = g_file_input_stream_query_info (data->in_stream,
782 G_FILE_ATTRIBUTE_STANDARD_SIZE ","
783 G_FILE_ATTRIBUTE_STANDARD_COPY_NAME ","
784 G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
785 G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME,
786 NULL,
787 &error);
788
789 /* If no stream information then probably using an old version of gvfs, fall back
790 * to getting the stream information from the GFile.
791 */
792 if (error != NULL) {
793 rb_debug ("file info query from input failed, trying query on file: %s", error->message);
794 g_error_free (error);
795
796 g_file_query_info_async (data->source,
797 G_FILE_ATTRIBUTE_STANDARD_SIZE ","
798 G_FILE_ATTRIBUTE_STANDARD_COPY_NAME ","
799 G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
800 G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME,
801 G_FILE_QUERY_INFO_NONE,
802 0,
803 data->cancel,
804 (GAsyncReadyCallback) download_file_info_cb,
805 data);
806 return;
807 }
808
809 rb_debug ("got file info results for %s",
810 get_remote_location (data->entry));
811
812 download_podcast (src_info, data);
813 }
814
815 static void
816 download_file_info_cb (GFile *source,
817 GAsyncResult *result,
818 RBPodcastManagerInfo *data)
819 {
820 GError *error = NULL;
821 GFileInfo *src_info;
822
823 src_info = g_file_query_info_finish (source, result, &error);
824
825 if (error != NULL) {
826 download_error (data, error);
827 g_error_free (error);
828 } else {
829 rb_debug ("got file info results for %s",
830 get_remote_location (data->entry));
831
832 download_podcast (src_info, data);
833 }
834 }
835
836 static void
837 download_podcast (GFileInfo *src_info, RBPodcastManagerInfo *data)
838 {
839 GError *error = NULL;
840 char *local_file_name = NULL;
841 char *feed_folder;
842 char *esc_local_file_name;
843 char *local_file_uri;
844 char *sane_local_file_uri;
845 char *conf_dir_uri;
846
847 if (src_info != NULL) {
848 data->download_size = g_file_info_get_attribute_uint64 (src_info, G_FILE_ATTRIBUTE_STANDARD_SIZE);
849
850 local_file_name = g_file_info_get_attribute_as_string (src_info, G_FILE_ATTRIBUTE_STANDARD_COPY_NAME);
851 if (local_file_name == NULL) {
852 /* probably shouldn't be using this, but the gvfs http backend doesn't
853 * set the copy name (yet)
854 */
855 local_file_name = g_strdup (g_file_info_get_edit_name (src_info));
856 }
857
858 g_object_unref (src_info);
859 }
860
861 if (local_file_name == NULL) {
862 /* fall back to the basename from the original URI */
863 local_file_name = g_file_get_basename (data->source);
864 rb_debug ("didn't get a filename from the file info request; using basename %s", local_file_name);
865 }
866
867 /* if the filename ends with the query string from the original URI,
868 * remove it.
869 */
870 if (data->query_string &&
871 g_str_has_suffix (local_file_name, data->query_string)) {
872 local_file_name[strlen (local_file_name) - strlen (data->query_string)] = '\0';
873 rb_debug ("removing query string \"%s\" -> local file name \"%s\"", data->query_string, local_file_name);
874 }
875
876 esc_local_file_name = g_uri_escape_string (local_file_name,
877 G_URI_RESERVED_CHARS_ALLOWED_IN_PATH,
878 TRUE);
879 feed_folder = g_uri_escape_string (rhythmdb_entry_get_string (data->entry, RHYTHMDB_PROP_ALBUM),
880 G_URI_RESERVED_CHARS_ALLOWED_IN_PATH,
881 TRUE);
882 g_strdelimit (feed_folder, "/", '_');
883 g_strdelimit (esc_local_file_name, "/", '_');
884
885 /* construct local filename */
886 conf_dir_uri = rb_podcast_manager_get_podcast_dir (data->pd);
887 local_file_uri = g_build_filename (conf_dir_uri,
888 feed_folder,
889 local_file_name,
890 NULL);
891
892 g_free (local_file_name);
893 g_free (feed_folder);
894 g_free (esc_local_file_name);
895
896 sane_local_file_uri = rb_sanitize_uri_for_filesystem (local_file_uri);
897 g_free (local_file_uri);
898
899 rb_debug ("download URI: %s", sane_local_file_uri);
900
901 if (rb_uri_create_parent_dirs (sane_local_file_uri, &error) == FALSE) {
902 rb_debug ("error creating parent dirs: %s", error->message);
903
904 rb_error_dialog (NULL, _("Error creating podcast download directory"),
905 _("Unable to create the download directory for %s: %s"),
906 sane_local_file_uri, error->message);
907
908 g_error_free (error);
909 rb_podcast_manager_abort_download (data);
910 return;
911 }
912
913 data->destination = g_file_new_for_uri (sane_local_file_uri);
914 if (g_file_query_exists (data->destination, NULL)) {
915 GFileInfo *dest_info;
916 guint64 local_size;
917
918 dest_info = g_file_query_info (data->destination,
919 G_FILE_ATTRIBUTE_STANDARD_SIZE,
920 G_FILE_QUERY_INFO_NONE,
921 NULL,
922 &error);
923 if (error != NULL) {
924 /* hrm */
925 g_warning ("Looking at downloaded podcast file %s: %s",
926 sane_local_file_uri, error->message);
927 g_error_free (error);
928 rb_podcast_manager_abort_download (data);
929 return;
930 }
931
932 /* check size */
933 local_size = g_file_info_get_attribute_uint64 (dest_info,
934 G_FILE_ATTRIBUTE_STANDARD_SIZE);
935 g_object_unref (dest_info);
936 if (local_size == data->download_size) {
937 GValue val = {0,};
938
939 rb_debug ("local file is the same size as the download (%" G_GUINT64_FORMAT ")",
940 local_size);
941
942 g_value_init (&val, G_TYPE_ULONG);
943 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_COMPLETE);
944 rhythmdb_entry_set (data->pd->priv->db, data->entry, RHYTHMDB_PROP_STATUS, &val);
945 g_value_unset (&val);
946
947 g_value_init (&val, G_TYPE_STRING);
948 g_value_take_string (&val, g_file_get_uri (data->destination));
949 set_download_location (data->pd->priv->db, data->entry, &val);
950 g_value_unset (&val);
951
952 rb_podcast_manager_save_metadata (data->pd, data->entry);
953
954 rb_podcast_manager_abort_download (data);
955 return;
956 } else if (local_size < data->download_size) {
957 rb_debug ("podcast partly downloaded (%" G_GUINT64_FORMAT " of %" G_GUINT64_FORMAT ")",
958 local_size, data->download_size);
959 data->download_offset = local_size;
960 } else {
961 rb_debug ("replacing local file as it's larger than the download");
962 g_file_delete (data->destination, NULL, &error);
963 if (error != NULL) {
964 g_warning ("Removing existing download: %s", error->message);
965 g_error_free (error);
966 rb_podcast_manager_abort_download (data);
967 return;
968 }
969 }
970 }
971
972 g_free (sane_local_file_uri);
973
974 GDK_THREADS_ENTER ();
975 g_signal_emit (data->pd, rb_podcast_manager_signals[START_DOWNLOAD],
976 0, data->entry);
977 GDK_THREADS_LEAVE ();
978
979 data->cancel = g_cancellable_new ();
980 data->thread = g_thread_new ("podcast-download",
981 (GThreadFunc) podcast_download_thread,
982 data);
983 }
984
985 static void
986 rb_podcast_manager_abort_download (RBPodcastManagerInfo *data)
987 {
988 RBPodcastManager *mgr = data->pd;
989
990 g_assert (rb_is_main_thread ());
991
992 mgr->priv->download_list = g_list_remove (mgr->priv->download_list, data);
993 download_info_free (data);
994
995 if (mgr->priv->active_download == data)
996 mgr->priv->active_download = NULL;
997
998 if (mgr->priv->next_file_id == 0) {
999 mgr->priv->next_file_id =
1000 g_idle_add ((GSourceFunc) rb_podcast_manager_next_file, mgr);
1001 }
1002 }
1003
1004 gboolean
1005 rb_podcast_manager_subscribe_feed (RBPodcastManager *pd, const char *url, gboolean automatic)
1006 {
1007 RBPodcastThreadInfo *info;
1008 GFile *feed;
1009 char *feed_url;
1010 gboolean existing_feed;
1011
1012 if (g_str_has_prefix (url, "feed://") || g_str_has_prefix (url, "itpc://")) {
1013 char *tmp;
1014
1015 tmp = g_strdup_printf ("http://%s", url + strlen ("feed://"));
1016 feed = g_file_new_for_uri (tmp);
1017 g_free (tmp);
1018 } else {
1019 feed = g_file_new_for_uri (url);
1020 }
1021
1022 /* hmm. can we check if the GFile we got is something useful? */
1023 #if 0
1024 if (valid_url == NULL) {
1025 rb_error_dialog (NULL, _("Invalid URL"),
1026 _("The URL \"%s\" is not valid, please check it."), url);
1027 return FALSE;
1028 }
1029 #endif
1030
1031 feed_url = g_file_get_uri (feed); /* not sure this buys us anything at all */
1032 RhythmDBEntry *entry = rhythmdb_entry_lookup_by_location (pd->priv->db, feed_url);
1033 if (entry) {
1034 if (rhythmdb_entry_get_entry_type (entry) != RHYTHMDB_ENTRY_TYPE_PODCAST_FEED) {
1035 /* added as something else, probably iradio */
1036 rb_error_dialog (NULL, _("URL already added"),
1037 _("The URL \"%s\" has already been added as a radio station. "
1038 "If this is a podcast feed, please remove the radio station."), url);
1039 return FALSE;
1040 }
1041 existing_feed = TRUE;
1042 } else {
1043 existing_feed = FALSE;
1044 }
1045
1046 info = g_new0 (RBPodcastThreadInfo, 1);
1047 info->pd = g_object_ref (pd);
1048 info->url = feed_url;
1049 info->automatic = automatic;
1050 info->existing_feed = existing_feed;
1051
1052 g_thread_new ("podcast-parse",
1053 (GThreadFunc) rb_podcast_manager_thread_parse_feed,
1054 info);
1055
1056 return TRUE;
1057 }
1058
1059 static void
1060 rb_podcast_manager_free_parse_result (RBPodcastManagerParseResult *result)
1061 {
1062 rb_podcast_parse_channel_free (result->channel);
1063 g_object_unref (result->pd);
1064 g_clear_error (&result->error);
1065 g_free (result);
1066 }
1067
1068 static gboolean
1069 rb_podcast_manager_parse_complete_cb (RBPodcastManagerParseResult *result)
1070 {
1071 GDK_THREADS_ENTER ();
1072 if (result->pd->priv->shutdown) {
1073 GDK_THREADS_LEAVE ();
1074 return FALSE;
1075 }
1076
1077 if (result->error) {
1078 rb_podcast_manager_handle_feed_error (result->pd,
1079 (char *)result->channel->url,
1080 result->error,
1081 (result->automatic == FALSE));
1082 } else {
1083 rb_podcast_manager_add_parsed_feed (result->pd, result->channel);
1084 }
1085
1086 GDK_THREADS_LEAVE ();
1087 return FALSE;
1088 }
1089
1090 static void
1091 confirm_bad_mime_type_response_cb (GtkDialog *dialog, int response, RBPodcastThreadInfo *info)
1092 {
1093 if (response == GTK_RESPONSE_YES) {
1094 /* set the 'existing feed' flag to avoid the mime type check */
1095 info->existing_feed = TRUE;
1096 rb_podcast_manager_thread_parse_feed (info);
1097 } else {
1098 g_free (info->url);
1099 g_free (info);
1100 }
1101
1102 gtk_widget_destroy (GTK_WIDGET (dialog));
1103 }
1104
1105 static void
1106 confirm_bad_mime_type (RBPodcastThreadInfo *info)
1107 {
1108 GtkWidget *dialog;
1109
1110 GDK_THREADS_ENTER ();
1111 dialog = gtk_message_dialog_new (NULL, 0,
1112 GTK_MESSAGE_QUESTION,
1113 GTK_BUTTONS_YES_NO,
1114 _("The URL '%s' does not appear to be a podcast feed. "
1115 "It may be the wrong URL, or the feed may be broken. "
1116 "Would you like Rhythmbox to attempt to use it anyway?"),
1117 info->url);
1118 gtk_widget_show_all (dialog);
1119 g_signal_connect (dialog, "response", G_CALLBACK (confirm_bad_mime_type_response_cb), info);
1120 GDK_THREADS_LEAVE ();
1121 }
1122
1123 static gpointer
1124 rb_podcast_manager_thread_parse_feed (RBPodcastThreadInfo *info)
1125 {
1126 RBPodcastChannel *feed = g_new0 (RBPodcastChannel, 1);
1127 RBPodcastManagerParseResult *result;
1128
1129 result = g_new0 (RBPodcastManagerParseResult, 1);
1130 result->channel = feed;
1131 result->pd = info->pd; /* adopts our reference */
1132 result->automatic = info->automatic;
1133
1134 g_clear_error (&result->error);
1135
1136 rb_debug ("attempting to parse feed %s", info->url);
1137 if (rb_podcast_parse_load_feed (feed, info->url, info->existing_feed, &result->error) == FALSE) {
1138 if (g_error_matches (result->error,
1139 RB_PODCAST_PARSE_ERROR,
1140 RB_PODCAST_PARSE_ERROR_MIME_TYPE)) {
1141 confirm_bad_mime_type (info);
1142 return NULL;
1143 }
1144 }
1145
1146 if (feed->is_opml) {
1147 GList *l;
1148
1149 rb_debug ("Loading OPML feeds from %s", info->url);
1150
1151 for (l = feed->posts; l != NULL; l = l->next) {
1152 RBPodcastItem *item = l->data;
1153 /* assume the feeds don't already exist */
1154 rb_podcast_manager_subscribe_feed (info->pd, item->url, FALSE);
1155 }
1156
1157 rb_podcast_manager_free_parse_result (result);
1158 } else {
1159 g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
1160 (GSourceFunc) rb_podcast_manager_parse_complete_cb,
1161 result,
1162 (GDestroyNotify) rb_podcast_manager_free_parse_result);
1163 }
1164
1165 g_free (info->url);
1166 g_free (info);
1167 return NULL;
1168 }
1169
1170 RhythmDBEntry *
1171 rb_podcast_manager_add_post (RhythmDB *db,
1172 gboolean search_result,
1173 const char *name,
1174 const char *title,
1175 const char *subtitle,
1176 const char *generator,
1177 const char *uri,
1178 const char *description,
1179 gulong date,
1180 gulong duration,
1181 guint64 filesize)
1182 {
1183 RhythmDBEntry *entry;
1184 RhythmDBEntryType *entry_type;
1185 GValue val = {0,};
1186 GTimeVal time;
1187
1188 if (!uri || !name || !title || !g_utf8_validate(uri, -1, NULL)) {
1189 return NULL;
1190 }
1191 entry = rhythmdb_entry_lookup_by_location (db, uri);
1192 if (entry)
1193 return NULL;
1194
1195 if (search_result == FALSE) {
1196 RhythmDBQueryModel *mountpoint_entries;
1197 GtkTreeIter iter;
1198
1199 /*
1200 * Does the uri exist as the mount-point?
1201 * This check is necessary since after an entry's file is downloaded,
1202 * the location stored in the db changes to the local file path
1203 * instead of the uri. The uri moves to the mount-point attribute.
1204 * Consequently, without this check, every downloaded entry will be
1205 * re-added to the db.
1206 */
1207 mountpoint_entries = rhythmdb_query_model_new_empty (db);
1208 g_object_set (mountpoint_entries, "show-hidden", TRUE, NULL);
1209 rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (mountpoint_entries),
1210 RHYTHMDB_QUERY_PROP_EQUALS,
1211 RHYTHMDB_PROP_TYPE,
1212 RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
1213 RHYTHMDB_QUERY_PROP_EQUALS,
1214 RHYTHMDB_PROP_MOUNTPOINT,
1215 uri,
1216 RHYTHMDB_QUERY_END);
1217
1218 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (mountpoint_entries), &iter)) {
1219 g_object_unref (mountpoint_entries);
1220 return NULL;
1221 }
1222 g_object_unref (mountpoint_entries);
1223 }
1224
1225 if (search_result)
1226 entry_type = RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH;
1227 else
1228 entry_type = RHYTHMDB_ENTRY_TYPE_PODCAST_POST;
1229
1230 entry = rhythmdb_entry_new (db,
1231 entry_type,
1232 uri);
1233 if (entry == NULL)
1234 return NULL;
1235
1236 g_get_current_time (&time);
1237 if (date == 0)
1238 date = time.tv_sec;
1239
1240 g_value_init (&val, G_TYPE_STRING);
1241 g_value_set_string (&val, name);
1242 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_ALBUM, &val);
1243
1244 g_value_reset (&val);
1245 g_value_set_static_string (&val, _("Podcast"));
1246 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_GENRE, &val);
1247
1248 g_value_reset (&val);
1249 g_value_set_string (&val, title);
1250 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_TITLE, &val);
1251
1252 g_value_reset (&val);
1253 if (subtitle)
1254 g_value_set_string (&val, subtitle);
1255 else
1256 g_value_set_static_string (&val, "");
1257 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_SUBTITLE, &val);
1258
1259 g_value_reset (&val);
1260 if (description)
1261 g_value_set_string (&val, description);
1262 else
1263 g_value_set_static_string (&val, "");
1264 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_DESCRIPTION, &val);
1265
1266 g_value_reset (&val);
1267 if (generator)
1268 g_value_set_string (&val, generator);
1269 else
1270 g_value_set_static_string (&val, "");
1271 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_ARTIST, &val);
1272 g_value_unset (&val);
1273
1274 g_value_init (&val, G_TYPE_ULONG);
1275 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_PAUSED);
1276 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_STATUS, &val);
1277
1278 g_value_reset (&val);
1279 g_value_set_ulong (&val, date);
1280 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_POST_TIME, &val);
1281
1282 g_value_reset (&val);
1283 g_value_set_ulong (&val, duration);
1284 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_DURATION, &val);
1285
1286 g_value_reset (&val);
1287 g_value_set_ulong (&val, 0);
1288 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_LAST_PLAYED, &val);
1289
1290 /* first seen */
1291 g_value_reset (&val);
1292 g_value_set_ulong (&val, time.tv_sec);
1293 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_FIRST_SEEN, &val);
1294 g_value_unset (&val);
1295
1296 /* initialize the rating */
1297 g_value_init (&val, G_TYPE_DOUBLE);
1298 g_value_set_double (&val, 0.0);
1299 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_RATING, &val);
1300 g_value_unset (&val);
1301
1302 g_value_init (&val, G_TYPE_UINT64);
1303 g_value_set_uint64 (&val, filesize);
1304 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_FILE_SIZE, &val);
1305 g_value_unset (&val);
1306
1307 return entry;
1308 }
1309
1310 typedef struct {
1311 RhythmDBEntry *entry;
1312 RBPodcastManager *mgr;
1313 } MissingPluginRetryData;
1314
1315 static void
1316 missing_plugins_retry_cb (gpointer inst, gboolean retry, MissingPluginRetryData *retry_data)
1317 {
1318 if (retry == FALSE)
1319 return;
1320
1321 rb_podcast_manager_save_metadata (retry_data->mgr, retry_data->entry);
1322 }
1323
1324 static void
1325 missing_plugins_retry_cleanup (MissingPluginRetryData *retry)
1326 {
1327 g_object_unref (retry->mgr);
1328 rhythmdb_entry_unref (retry->entry);
1329 g_free (retry);
1330 }
1331
1332
1333 static void
1334 rb_podcast_manager_save_metadata (RBPodcastManager *pd, RhythmDBEntry *entry)
1335 {
1336 RBMetaData *md = rb_metadata_new ();
1337 GError *error = NULL;
1338 GValue val = { 0, };
1339 const char *media_type;
1340 const char *uri;
1341 char **missing_plugins;
1342 char **plugin_descriptions;
1343
1344 uri = get_download_location (entry);
1345 rb_debug ("loading podcast metadata from %s", uri);
1346 rb_metadata_load (md, uri, &error);
1347
1348 if (rb_metadata_get_missing_plugins (md, &missing_plugins, &plugin_descriptions)) {
1349 GClosure *closure;
1350 gboolean processing;
1351 MissingPluginRetryData *data;
1352
1353 rb_debug ("missing plugins during podcast metadata load for %s", uri);
1354 data = g_new0 (MissingPluginRetryData, 1);
1355 data->mgr = g_object_ref (pd);
1356 data->entry = rhythmdb_entry_ref (entry);
1357
1358 closure = g_cclosure_new ((GCallback) missing_plugins_retry_cb,
1359 data,
1360 (GClosureNotify) missing_plugins_retry_cleanup);
1361 g_closure_set_marshal (closure, g_cclosure_marshal_VOID__BOOLEAN);
1362
1363 processing = rb_missing_plugins_install ((const char **)missing_plugins, FALSE, closure);
1364 g_closure_sink (closure);
1365
1366 if (processing) {
1367 /* when processing is complete, we'll retry */
1368 return;
1369 }
1370 }
1371
1372 if (error != NULL) {
1373 /* this probably isn't an audio enclosure. or some other error */
1374 g_value_init (&val, G_TYPE_ULONG);
1375 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_ERROR);
1376 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
1377 g_value_unset (&val);
1378
1379 g_value_init (&val, G_TYPE_STRING);
1380 g_value_set_string (&val, error->message);
1381 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &val);
1382 g_value_unset (&val);
1383
1384 rhythmdb_commit (pd->priv->db);
1385
1386 g_object_unref (md);
1387 g_error_free (error);
1388
1389 return;
1390 }
1391
1392 media_type = rb_metadata_get_media_type (md);
1393 if (media_type) {
1394 g_value_init (&val, G_TYPE_STRING);
1395 g_value_set_string (&val, media_type);
1396 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_MEDIA_TYPE, &val);
1397 g_value_unset (&val);
1398 }
1399
1400 if (rb_metadata_get (md,
1401 RB_METADATA_FIELD_DURATION,
1402 &val)) {
1403 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_DURATION, &val);
1404 g_value_unset (&val);
1405 }
1406
1407 if (rb_metadata_get (md,
1408 RB_METADATA_FIELD_BITRATE,
1409 &val)) {
1410 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_BITRATE, &val);
1411 g_value_unset (&val);
1412 }
1413
1414 rhythmdb_commit (pd->priv->db);
1415
1416 g_object_unref (md);
1417 }
1418
1419 static void
1420 rb_podcast_manager_db_entry_added_cb (RBPodcastManager *pd, RhythmDBEntry *entry)
1421 {
1422 RhythmDBEntryType *type = rhythmdb_entry_get_entry_type (entry);
1423
1424 if (type != RHYTHMDB_ENTRY_TYPE_PODCAST_POST)
1425 return;
1426
1427 rb_podcast_manager_download_entry (pd, entry);
1428 }
1429
1430 static void
1431 download_info_free (RBPodcastManagerInfo *data)
1432 {
1433 /* what should this do about the thread and etc.? */
1434
1435 if (data->cancel != NULL) {
1436 g_object_unref (data->cancel);
1437 data->cancel = NULL;
1438 }
1439
1440 if (data->source) {
1441 g_object_unref (data->source);
1442 data->source = NULL;
1443 }
1444
1445 if (data->destination) {
1446 g_object_unref (data->destination);
1447 data->destination = NULL;
1448 }
1449
1450 if (data->query_string) {
1451 g_free (data->query_string);
1452 data->query_string = NULL;
1453 }
1454
1455 if (data->entry) {
1456 rhythmdb_entry_unref (data->entry);
1457 }
1458
1459 g_free (data);
1460 }
1461
1462 static void
1463 download_progress (RBPodcastManagerInfo *data, guint64 downloaded, guint64 total, gboolean complete)
1464 {
1465 guint local_progress = 0;
1466
1467 if (downloaded > 0 && total > 0)
1468 local_progress = (100 * downloaded) / total;
1469
1470 if (local_progress != data->progress) {
1471 GValue val = {0,};
1472
1473 rb_debug ("%s: %" G_GUINT64_FORMAT "/ %" G_GUINT64_FORMAT,
1474 rhythmdb_entry_get_string (data->entry, RHYTHMDB_PROP_LOCATION),
1475 downloaded, total);
1476
1477 GDK_THREADS_ENTER ();
1478
1479 g_value_init (&val, G_TYPE_ULONG);
1480 g_value_set_ulong (&val, local_progress);
1481 rhythmdb_entry_set (data->pd->priv->db, data->entry, RHYTHMDB_PROP_STATUS, &val);
1482 g_value_unset (&val);
1483
1484 rhythmdb_commit (data->pd->priv->db);
1485
1486 GDK_THREADS_LEAVE ();
1487
1488 data->progress = local_progress;
1489 }
1490
1491 if (complete) {
1492 if (g_cancellable_is_cancelled (data->cancel) == FALSE) {
1493 GValue val = {0,};
1494 rb_debug ("download of %s completed",
1495 get_remote_location (data->entry));
1496
1497 g_value_init (&val, G_TYPE_UINT64);
1498 g_value_set_uint64 (&val, downloaded);
1499 rhythmdb_entry_set (data->pd->priv->db, data->entry, RHYTHMDB_PROP_FILE_SIZE, &val);
1500 g_value_unset (&val);
1501
1502 if (total == 0 || downloaded >= total) {
1503 g_value_init (&val, G_TYPE_ULONG);
1504 g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_COMPLETE);
1505 rhythmdb_entry_set (data->pd->priv->db, data->entry, RHYTHMDB_PROP_STATUS, &val);
1506 g_value_unset (&val);
1507 }
1508
1509 rb_podcast_manager_save_metadata (data->pd,
1510 data->entry);
1511 }
1512 g_idle_add ((GSourceFunc)end_job, data);
1513 }
1514 }
1515
1516 static gpointer
1517 podcast_download_thread (RBPodcastManagerInfo *data)
1518 {
1519 GError *error = NULL;
1520 char buf[8192];
1521 gssize n_read;
1522 gssize n_written;
1523 guint64 downloaded;
1524
1525 /* if we have an offset to download from, try the seek
1526 * before anything else. if we can't seek, we'll have to
1527 * grab the whole thing.
1528 */
1529 downloaded = 0;
1530 if (data->download_offset != 0) {
1531 g_seekable_seek (G_SEEKABLE (data->in_stream),
1532 data->download_offset,
1533 G_SEEK_SET,
1534 data->cancel,
1535 &error);
1536 if (error == NULL) {
1537 /* ok, now we can open the output file for appending */
1538 rb_debug ("seek to offset %" G_GUINT64_FORMAT " successful", data->download_offset);
1539 data->out_stream = g_file_append_to (data->destination,
1540 G_FILE_CREATE_NONE,
1541 data->cancel,
1542 &error);
1543 downloaded = data->download_offset;
1544 } else if (error->domain == G_IO_ERROR &&
1545 error->code == G_IO_ERROR_NOT_SUPPORTED) {
1546 /* can't seek, download the whole thing */
1547 rb_debug ("seeking failed: %s", error->message);
1548 g_clear_error (&error);
1549 }
1550 }
1551 if (error != NULL) {
1552 download_error (data, error);
1553 g_error_free (error);
1554 return NULL;
1555 }
1556
1557 /* set the downloaded location for the episode
1558 * and do it before opening the file, so that the monitor
1559 * doesn't add a normal entry for us
1560 */
1561 if (get_download_location (data->entry) == NULL) {
1562 GValue val = {0,};
1563 char *uri = g_file_get_uri (data->destination);
1564
1565 g_value_init (&val, G_TYPE_STRING);
1566 g_value_set_string (&val, uri);
1567 set_download_location (data->pd->priv->db, data->entry, &val);
1568 g_value_unset (&val);
1569
1570 rhythmdb_commit (data->pd->priv->db);
1571 g_free (uri);
1572 }
1573
1574 /* gvfs doesn't do file info queries from streams, so this doesn't help, but
1575 * maybe some day it will..
1576 */
1577 if (data->download_size == 0) {
1578 GFileInfo *info;
1579
1580 info = g_file_input_stream_query_info (data->in_stream,
1581 G_FILE_ATTRIBUTE_STANDARD_SIZE,
1582 NULL,
1583 &error);
1584 if (error != NULL) {
1585 rb_debug ("stream info query failed: %s", error->message);
1586 g_clear_error (&error);
1587 } else {
1588 data->download_size = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_SIZE);
1589 rb_debug ("got file size from stream: %" G_GINT64_FORMAT, data->download_size);
1590 g_object_unref (info);
1591 }
1592 }
1593
1594 /* open local file */
1595 if (data->out_stream == NULL) {
1596 data->out_stream = g_file_create (data->destination,
1597 G_FILE_CREATE_NONE,
1598 data->cancel,
1599 &error);
1600 if (error != NULL) {
1601 download_error (data, error);
1602 g_error_free (error);
1603 return NULL;
1604 }
1605 }
1606
1607 /* loop, copying from input stream to output stream */
1608 while (TRUE) {
1609 char *p;
1610 n_read = g_input_stream_read (G_INPUT_STREAM (data->in_stream),
1611 buf, sizeof (buf),
1612 data->cancel,
1613 &error);
1614 if (n_read < 1) {
1615 break;
1616 }
1617
1618 p = buf;
1619 while (n_read > 0) {
1620 n_written = g_output_stream_write (G_OUTPUT_STREAM (data->out_stream),
1621 p, n_read,
1622 data->cancel,
1623 &error);
1624 if (n_written == -1) {
1625 break;
1626 }
1627 p += n_written;
1628 n_read -= n_written;
1629 downloaded += n_written;
1630 }
1631 if (n_written == -1)
1632 break;
1633
1634 download_progress (data, downloaded, data->download_size, FALSE);
1635 }
1636
1637 /* close everything - don't allow these operations to be cancelled */
1638 g_input_stream_close (G_INPUT_STREAM (data->in_stream), NULL, NULL);
1639 g_object_unref (data->in_stream);
1640
1641 g_output_stream_close (G_OUTPUT_STREAM (data->out_stream), NULL, &error);
1642 g_object_unref (data->out_stream);
1643
1644 if (error != NULL) {
1645 download_error (data, error);
1646 g_error_free (error);
1647 } else {
1648 download_progress (data, downloaded, data->download_size, TRUE);
1649 }
1650
1651 rb_debug ("exiting download thread");
1652 return NULL;
1653 }
1654
1655 static gboolean
1656 end_job (RBPodcastManagerInfo *data)
1657 {
1658 RBPodcastManager *pd = data->pd;
1659
1660 g_assert (rb_is_main_thread ());
1661
1662 rb_debug ("cleaning up download of %s",
1663 get_remote_location (data->entry));
1664
1665 data->pd->priv->download_list = g_list_remove (data->pd->priv->download_list, data);
1666
1667 GDK_THREADS_ENTER ();
1668 g_signal_emit (data->pd, rb_podcast_manager_signals[FINISH_DOWNLOAD],
1669 0, data->entry);
1670 GDK_THREADS_LEAVE ();
1671
1672 g_assert (pd->priv->active_download == data);
1673 pd->priv->active_download = NULL;
1674
1675 download_info_free (data);
1676
1677 if (pd->priv->next_file_id == 0) {
1678 pd->priv->next_file_id =
1679 g_idle_add ((GSourceFunc) rb_podcast_manager_next_file, pd);
1680 }
1681 return FALSE;
1682 }
1683
1684 static void
1685 cancel_job (RBPodcastManagerInfo *data)
1686 {
1687 g_assert (rb_is_main_thread ());
1688 rb_debug ("cancelling download of %s", get_remote_location (data->entry));
1689
1690 /* is this the active download? */
1691 if (data == data->pd->priv->active_download) {
1692 g_cancellable_cancel (data->cancel);
1693
1694 /* download data will be cleaned up after next progress callback */
1695 } else {
1696 /* destroy download data */
1697 data->pd->priv->download_list = g_list_remove (data->pd->priv->download_list, data);
1698 download_info_free (data);
1699 }
1700 }
1701
1702
1703 void
1704 rb_podcast_manager_unsubscribe_feed (RhythmDB *db, const char *url)
1705 {
1706 RhythmDBEntry *entry = rhythmdb_entry_lookup_by_location (db, url);
1707 if (entry) {
1708 GValue val = {0, };
1709 g_value_init (&val, G_TYPE_ULONG);
1710 g_value_set_ulong (&val, 0);
1711 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_STATUS, &val);
1712 g_value_unset (&val);
1713 }
1714
1715 }
1716
1717 gboolean
1718 rb_podcast_manager_remove_feed (RBPodcastManager *pd, const char *url, gboolean remove_files)
1719 {
1720 RhythmDBQueryModel *query;
1721 GtkTreeModel *query_model;
1722 GtkTreeIter iter;
1723 RhythmDBEntry *entry;
1724
1725 entry = rhythmdb_entry_lookup_by_location (pd->priv->db, url);
1726 if (entry == NULL) {
1727 rb_debug ("unable to find entry for podcast feed %s", url);
1728 return FALSE;
1729 }
1730
1731 rb_debug ("removing podcast feed: %s remove_files: %d", url, remove_files);
1732
1733 /* first remove the posts from the feed. include deleted posts (which will be hidden).
1734 * these need to be deleted so they will properly be readded should the feed be readded.
1735 */
1736 query = rhythmdb_query_model_new_empty (pd->priv->db);
1737 g_object_set (query, "show-hidden", TRUE, NULL);
1738 query_model = GTK_TREE_MODEL (query);
1739 rhythmdb_do_full_query (pd->priv->db,
1740 RHYTHMDB_QUERY_RESULTS (query_model),
1741 RHYTHMDB_QUERY_PROP_EQUALS,
1742 RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
1743 RHYTHMDB_QUERY_PROP_LIKE,
1744 RHYTHMDB_PROP_SUBTITLE, get_remote_location (entry),
1745 RHYTHMDB_QUERY_END);
1746
1747 if (gtk_tree_model_get_iter_first (query_model, &iter)) {
1748 gboolean has_next;
1749 do {
1750 RhythmDBEntry *entry;
1751
1752 gtk_tree_model_get (query_model, &iter, 0, &entry, -1);
1753 has_next = gtk_tree_model_iter_next (query_model, &iter);
1754
1755 /* make sure we're not downloading it */
1756 rb_podcast_manager_cancel_download (pd, entry);
1757 if (remove_files) {
1758 rb_podcast_manager_delete_download (pd, entry);
1759 }
1760
1761 rhythmdb_entry_delete (pd->priv->db, entry);
1762 rhythmdb_entry_unref (entry);
1763
1764 } while (has_next);
1765
1766 rhythmdb_commit (pd->priv->db);
1767 }
1768
1769 g_object_unref (query_model);
1770
1771 /* now delete the feed */
1772 rhythmdb_entry_delete (pd->priv->db, entry);
1773 rhythmdb_commit (pd->priv->db);
1774 return TRUE;
1775 }
1776
1777 void
1778 rb_podcast_manager_delete_download (RBPodcastManager *pd, RhythmDBEntry *entry)
1779 {
1780 const char *file_name;
1781 GFile *file;
1782 GError *error = NULL;
1783 RhythmDBEntryType *type = rhythmdb_entry_get_entry_type (entry);
1784
1785 /* make sure it's a podcast post */
1786 g_assert (type == RHYTHMDB_ENTRY_TYPE_PODCAST_POST);
1787
1788 file_name = get_download_location (entry);
1789 if (file_name == NULL) {
1790 /* episode has not been downloaded */
1791 rb_debug ("Episode %s not downloaded",
1792 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION));
1793 return;
1794 }
1795
1796 rb_debug ("deleting downloaded episode %s", file_name);
1797 file = g_file_new_for_uri (file_name);
1798 g_file_delete (file, NULL, &error);
1799
1800 if (error != NULL) {
1801 rb_debug ("Removing episode failed: %s", error->message);
1802 g_clear_error (&error);
1803 } else {
1804 GFile *feed_dir;
1805 /* try to remove the directory
1806 * (will only work once it's empty)
1807 */
1808 feed_dir = g_file_get_parent (file);
1809 g_file_delete (feed_dir, NULL, &error);
1810 if (error != NULL) {
1811 rb_debug ("couldn't remove podcast feed directory: %s",
1812 error->message);
1813 g_clear_error (&error);
1814 }
1815 g_object_unref (feed_dir);
1816 }
1817 g_object_unref (file);
1818 }
1819
1820 void
1821 rb_podcast_manager_cancel_download (RBPodcastManager *pd, RhythmDBEntry *entry)
1822 {
1823 GList *lst;
1824 g_assert (rb_is_main_thread ());
1825
1826 for (lst = pd->priv->download_list; lst != NULL; lst = lst->next) {
1827 RBPodcastManagerInfo *data = (RBPodcastManagerInfo *) lst->data;
1828 if (data->entry == entry) {
1829 cancel_job (data);
1830 return;
1831 }
1832 }
1833 }
1834
1835 static void
1836 podcast_settings_changed_cb (GSettings *settings, const char *key, RBPodcastManager *mgr)
1837 {
1838 if (g_strcmp0 (key, PODCAST_DOWNLOAD_INTERVAL) == 0) {
1839 rb_podcast_manager_start_update_timer (mgr);
1840 }
1841 }
1842
1843 /* this bit really wants to die */
1844
1845 static gboolean
1846 remove_if_not_downloaded (GtkTreeModel *model,
1847 GtkTreePath *path,
1848 GtkTreeIter *iter,
1849 GList **remove)
1850 {
1851 RhythmDBEntry *entry;
1852
1853 entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (model),
1854 iter);
1855 if (entry != NULL) {
1856 if (rb_podcast_manager_entry_downloaded (entry) == FALSE) {
1857 rb_debug ("entry %s is no longer present in the feed and has not been downloaded",
1858 get_remote_location (entry));
1859 *remove = g_list_prepend (*remove, entry);
1860 } else {
1861 rhythmdb_entry_unref (entry);
1862 }
1863 }
1864
1865 return FALSE;
1866 }
1867
1868 void
1869 rb_podcast_manager_insert_feed_url (RBPodcastManager *pd, const char *url)
1870 {
1871 RhythmDBEntry *entry;
1872 GValue status_val = { 0, };
1873 GValue title_val = { 0, };
1874 GValue author_val = { 0, };
1875 GValue last_update_val = { 0, };
1876
1877 entry = rhythmdb_entry_lookup_by_location (pd->priv->db, url);
1878 if (entry) {
1879 rb_debug ("podcast feed entry for %s found", url);
1880 return;
1881 }
1882 rb_debug ("adding podcast feed %s with no entries", url);
1883 entry = rhythmdb_entry_new (pd->priv->db,
1884 RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
1885 url);
1886 if (entry == NULL)
1887 return;
1888
1889 g_value_init (&status_val, G_TYPE_ULONG);
1890 g_value_set_ulong (&status_val, 1);
1891 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_STATUS, &status_val);
1892 g_value_unset (&status_val);
1893
1894 g_value_init (&title_val, G_TYPE_STRING);
1895 g_value_set_string (&title_val, url);
1896 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_TITLE, &title_val);
1897 g_value_unset (&title_val);
1898
1899 g_value_init (&author_val, G_TYPE_STRING);
1900 g_value_set_static_string (&author_val, _("Unknown"));
1901 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_ARTIST, &author_val);
1902 g_value_unset (&author_val);
1903
1904 g_value_init (&last_update_val, G_TYPE_ULONG);
1905 g_value_set_ulong (&last_update_val, time(NULL));
1906 rhythmdb_entry_set (pd->priv->db, entry, RHYTHMDB_PROP_LAST_SEEN, &last_update_val);
1907 g_value_unset (&last_update_val);
1908 }
1909
1910 void
1911 rb_podcast_manager_add_parsed_feed (RBPodcastManager *pd, RBPodcastChannel *data)
1912 {
1913 GValue description_val = { 0, };
1914 GValue title_val = { 0, };
1915 GValue lang_val = { 0, };
1916 GValue copyright_val = { 0, };
1917 GValue image_val = { 0, };
1918 GValue author_val = { 0, };
1919 GValue status_val = { 0, };
1920 GValue last_post_val = { 0, };
1921 GValue last_update_val = { 0, };
1922 GValue error_val = { 0, };
1923 gulong last_post = 0;
1924 gulong new_last_post;
1925 const char *title;
1926 GList *download_entries = NULL;
1927 gboolean new_feed, updated, download_last;
1928 RhythmDB *db = pd->priv->db;
1929 RhythmDBQueryModel *existing_entries = NULL;
1930
1931 RhythmDBEntry *entry;
1932
1933 GList *lst_songs;
1934
1935 new_feed = TRUE;
1936
1937 /* processing podcast head */
1938 entry = rhythmdb_entry_lookup_by_location (db, (gchar *)data->url);
1939 if (entry) {
1940 if (rhythmdb_entry_get_entry_type (entry) != RHYTHMDB_ENTRY_TYPE_PODCAST_FEED)
1941 return;
1942
1943 rb_debug ("Podcast feed entry for %s found", data->url);
1944 g_value_init (&status_val, G_TYPE_ULONG);
1945 g_value_set_ulong (&status_val, 1);
1946 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_STATUS, &status_val);
1947 g_value_unset (&status_val);
1948 last_post = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_POST_TIME);
1949 new_feed = FALSE;
1950
1951 /* find all the existing entries in this feed, so we can cull those
1952 * that haven't been downloaded and are no longer present in the feed.
1953 */
1954 existing_entries = rhythmdb_query_model_new_empty (db);
1955 g_object_set (existing_entries, "show-hidden", TRUE, NULL);
1956 rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (existing_entries),
1957 RHYTHMDB_QUERY_PROP_EQUALS,
1958 RHYTHMDB_PROP_TYPE,
1959 RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
1960 RHYTHMDB_QUERY_PROP_EQUALS,
1961 RHYTHMDB_PROP_SUBTITLE,
1962 data->url,
1963 RHYTHMDB_QUERY_END);
1964 } else {
1965 rb_debug ("Adding podcast feed: %s", data->url);
1966 entry = rhythmdb_entry_new (db,
1967 RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
1968 (gchar *) data->url);
1969 if (entry == NULL)
1970 return;
1971
1972 g_value_init (&status_val, G_TYPE_ULONG);
1973 g_value_set_ulong (&status_val, 1);
1974 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_STATUS, &status_val);
1975 g_value_unset (&status_val);
1976 }
1977
1978 /* if the feed does not contain a title, use the URL instead */
1979 g_value_init (&title_val, G_TYPE_STRING);
1980 if (data->title == NULL || strlen ((gchar *)data->title) == 0) {
1981 g_value_set_string (&title_val, (gchar *) data->url);
1982 title = data->url;
1983 } else {
1984 g_value_set_string (&title_val, (gchar *) data->title);
1985 title = data->title;
1986 }
1987 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_TITLE, &title_val);
1988 g_value_unset (&title_val);
1989
1990 g_value_init (&author_val, G_TYPE_STRING);
1991 if (data->author)
1992 g_value_set_string (&author_val, (gchar *) data->author);
1993 else
1994 g_value_set_static_string (&author_val, _("Unknown"));
1995 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_ARTIST, &author_val);
1996 g_value_unset (&author_val);
1997
1998 if (data->description) {
1999 g_value_init (&description_val, G_TYPE_STRING);
2000 g_value_set_string (&description_val, (gchar *) data->description);
2001 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_DESCRIPTION, &description_val);
2002 g_value_unset (&description_val);
2003 }
2004
2005 if (data->lang) {
2006 g_value_init (&lang_val, G_TYPE_STRING);
2007 g_value_set_string (&lang_val, (gchar *) data->lang);
2008 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_LANG, &lang_val);
2009 g_value_unset (&lang_val);
2010 }
2011
2012 if (data->copyright) {
2013 g_value_init (©right_val, G_TYPE_STRING);
2014 g_value_set_string (©right_val, (gchar *) data->copyright);
2015 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_COPYRIGHT, ©right_val);
2016 g_value_unset (©right_val);
2017 }
2018
2019 if (data->img) {
2020 RBExtDBKey *key;
2021
2022 g_value_init (&image_val, G_TYPE_STRING);
2023 g_value_set_string (&image_val, (gchar *) data->img);
2024 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_IMAGE, &image_val);
2025 g_value_unset (&image_val);
2026
2027 key = rb_ext_db_key_create_storage ("album", rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE));
2028 rb_ext_db_store_uri (pd->priv->art_store,
2029 key,
2030 RB_EXT_DB_SOURCE_SEARCH, /* sort of */
2031 data->img);
2032 }
2033
2034 /* clear any error that might have been set earlier */
2035 g_value_init (&error_val, G_TYPE_STRING);
2036 g_value_set_string (&error_val, NULL);
2037 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &error_val);
2038 g_value_unset (&error_val);
2039
2040 /* insert episodes */
2041 new_last_post = last_post;
2042
2043 updated = FALSE;
2044 download_last = (g_settings_get_enum (pd->priv->settings, PODCAST_DOWNLOAD_INTERVAL) != PODCAST_INTERVAL_MANUAL);
2045 for (lst_songs = data->posts; lst_songs != NULL; lst_songs = g_list_next (lst_songs)) {
2046 RBPodcastItem *item = (RBPodcastItem *) lst_songs->data;
2047 RhythmDBEntry *post_entry;
2048
2049 if (existing_entries != NULL) {
2050 GtkTreeIter iter;
2051 RhythmDBEntry *entry = NULL;
2052
2053 /* look for an existing entry with this remote location */
2054 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (existing_entries), &iter)) {
2055 do {
2056 entry = rhythmdb_query_model_iter_to_entry (existing_entries, &iter);
2057 if (strcmp (get_remote_location (entry), item->url) == 0) {
2058 rhythmdb_entry_unref (entry);
2059 break;
2060 }
2061 rhythmdb_entry_unref (entry);
2062 entry = NULL;
2063
2064 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (existing_entries), &iter));
2065 }
2066
2067 if (entry != NULL) {
2068 /* mark this entry as still being available */
2069 rhythmdb_query_model_remove_entry (existing_entries, entry);
2070 }
2071 }
2072
2073
2074 post_entry =
2075 rb_podcast_manager_add_post (db,
2076 FALSE,
2077 title,
2078 (gchar *) item->title,
2079 (gchar *) data->url,
2080 (gchar *) (item->author ? item->author : data->author),
2081 (gchar *) item->url,
2082 (gchar *) item->description,
2083 (gulong) (item->pub_date > 0 ? item->pub_date : data->pub_date),
2084 (gulong) item->duration,
2085 item->filesize);
2086
2087 if (post_entry)
2088 updated = TRUE;
2089
2090 if (post_entry && item->pub_date >= new_last_post) {
2091 if (item->pub_date > new_last_post) {
2092 g_list_free (download_entries);
2093 download_entries = NULL;
2094 }
2095 download_entries = g_list_prepend (download_entries, post_entry);
2096 new_last_post = item->pub_date;
2097 }
2098 }
2099
2100 if (download_last) {
2101 GValue status = {0,};
2102 GList *t;
2103
2104 g_value_init (&status, G_TYPE_ULONG);
2105 g_value_set_ulong (&status, RHYTHMDB_PODCAST_STATUS_WAITING);
2106 for (t = download_entries; t != NULL; t = g_list_next (t)) {
2107 rhythmdb_entry_set (db,
2108 (RhythmDBEntry*) t->data,
2109 RHYTHMDB_PROP_STATUS,
2110 &status);
2111 }
2112 g_value_unset (&status);
2113 }
2114 g_list_free (download_entries);
2115
2116 if (updated)
2117 g_signal_emit (pd, rb_podcast_manager_signals[FEED_UPDATES_AVAILABLE],
2118 0, entry);
2119
2120 if (data->pub_date > new_last_post)
2121 new_last_post = data->pub_date;
2122
2123 g_value_init (&last_post_val, G_TYPE_ULONG);
2124 g_value_set_ulong (&last_post_val, new_last_post);
2125
2126 if (new_feed)
2127 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_POST_TIME, &last_post_val);
2128 else
2129 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_POST_TIME, &last_post_val);
2130 g_value_unset (&last_post_val);
2131
2132 g_value_init (&last_update_val, G_TYPE_ULONG);
2133 g_value_set_ulong (&last_update_val, time(NULL));
2134
2135 if (new_feed)
2136 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_LAST_SEEN, &last_update_val);
2137 else
2138 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_LAST_SEEN, &last_update_val);
2139 g_value_unset (&last_update_val);
2140
2141 if (existing_entries != NULL) {
2142 GList *remove = NULL;
2143 GList *i;
2144
2145 /* look for expired entries to remove */
2146 gtk_tree_model_foreach (GTK_TREE_MODEL (existing_entries),
2147 (GtkTreeModelForeachFunc) remove_if_not_downloaded,
2148 &remove);
2149 for (i = remove; i != NULL; i = i->next) {
2150 rhythmdb_entry_delete (db, (RhythmDBEntry *)i->data);
2151 }
2152 g_list_free (remove);
2153
2154 g_object_unref (existing_entries);
2155 }
2156
2157 rhythmdb_commit (db);
2158 }
2159
2160 static void
2161 rb_podcast_manager_handle_feed_error (RBPodcastManager *mgr,
2162 const char *url,
2163 GError *error,
2164 gboolean emit)
2165 {
2166 RhythmDBEntry *entry;
2167 GValue v = {0,};
2168 gboolean existing = FALSE;
2169
2170 /* set the error in the feed entry, if one exists */
2171 entry = rhythmdb_entry_lookup_by_location (mgr->priv->db, url);
2172 if (entry != NULL && rhythmdb_entry_get_entry_type (entry) == RHYTHMDB_ENTRY_TYPE_PODCAST_FEED) {
2173 g_value_init (&v, G_TYPE_STRING);
2174 g_value_set_string (&v, error->message);
2175 rhythmdb_entry_set (mgr->priv->db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &v);
2176 g_value_unset (&v);
2177
2178 rhythmdb_commit (mgr->priv->db);
2179 existing = TRUE;
2180 }
2181
2182 /* if this was a result of a direct user action, emit the error signal too */
2183 if (emit) {
2184 gchar *error_msg;
2185 error_msg = g_strdup_printf (_("There was a problem adding this podcast: %s. Please verify the URL: %s"),
2186 error->message, url);
2187 g_signal_emit (mgr,
2188 rb_podcast_manager_signals[PROCESS_ERROR],
2189 0, url, error_msg, existing);
2190 g_free (error_msg);
2191 }
2192 }
2193
2194 void
2195 rb_podcast_manager_shutdown (RBPodcastManager *pd)
2196 {
2197 GList *lst, *l;
2198
2199 g_assert (rb_is_main_thread ());
2200
2201 lst = g_list_reverse (g_list_copy (pd->priv->download_list));
2202 for (l = lst; l != NULL; l = l->next) {
2203 RBPodcastManagerInfo *data = (RBPodcastManagerInfo *) l->data;
2204 cancel_job (data);
2205 }
2206 g_list_free (lst);
2207
2208 pd->priv->shutdown = TRUE;
2209 }
2210
2211 char *
2212 rb_podcast_manager_get_podcast_dir (RBPodcastManager *pd)
2213 {
2214 char *conf_dir_uri = g_settings_get_string (pd->priv->settings, PODCAST_DOWNLOAD_DIR_KEY);
2215
2216 /* if we don't have a download directory yet, use the music dir,
2217 * or the home dir if we can't find that.
2218 */
2219 if (conf_dir_uri == NULL || (strcmp (conf_dir_uri, "") == 0)) {
2220 const char *conf_dir_name;
2221
2222 conf_dir_name = g_get_user_special_dir (G_USER_DIRECTORY_MUSIC);
2223 if (!conf_dir_name)
2224 conf_dir_name = g_get_home_dir ();
2225
2226 conf_dir_uri = g_filename_to_uri (conf_dir_name, NULL, NULL);
2227 g_settings_set_string (pd->priv->settings, PODCAST_DOWNLOAD_DIR_KEY, conf_dir_uri);
2228 }
2229
2230 return conf_dir_uri;
2231 }
2232
2233 void
2234 rb_podcast_manager_add_search (RBPodcastManager *pd, GType search_type)
2235 {
2236 pd->priv->searches = g_list_append (pd->priv->searches, GUINT_TO_POINTER (search_type));
2237 }
2238
2239 GList *
2240 rb_podcast_manager_get_searches (RBPodcastManager *pd)
2241 {
2242 GList *searches = NULL;
2243 GList *i;
2244
2245 for (i = pd->priv->searches; i != NULL; i = i->next) {
2246 RBPodcastSearch *search;
2247 GType search_type;
2248
2249 search_type = GPOINTER_TO_UINT (i->data);
2250 search = RB_PODCAST_SEARCH (g_object_new (search_type, NULL));
2251 searches = g_list_append (searches, search);
2252 }
2253
2254 return searches;
2255 }