totem-grilo.c 79.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
/* -*- Mode: C; indent-tabs-mode: t -*- */

/*
 * Copyright (C) 2010, 2011 Igalia S.L. <info@igalia.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
 *
 * The Totem project hereby grant permission for non-GPL compatible GStreamer
 * plugins to be used and distributed together with GStreamer and Totem. This
 * permission are above and beyond the permissions granted by the GPL license
 * Totem is covered by.
 *
 * See license_change file for details.
 */

#include "config.h"
29
#include "icon-helpers.h"
30 31 32 33 34 35 36

#include <time.h>

#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <grilo.h>
37
#include <pls/grl-pls.h>
38 39 40 41

#include <totem-interface.h>
#include <totem-dirs.h>
#include <totem.h>
42
#include <totem-private.h>
43

44
#include <totem-time-helpers.h>
45

46
#include "totem-grilo.h"
47
#include "totem-search-entry.h"
48
#include "totem-main-toolbar.h"
49
#include "totem-selection-toolbar.h"
50
#include <libgd/gd.h>
51

52 53 54
#define BROWSE_FLAGS          (GRL_RESOLVE_FAST_ONLY | GRL_RESOLVE_IDLE_RELAY)
#define PAGE_SIZE             50
#define SCROLL_GET_MORE_LIMIT 0.8
55
#define MIN_DURATION          5
56

57 58 59 60 61 62
/* casts are to shut gcc up */
static const GtkTargetEntry target_table[] = {
	{ (gchar*) "text/uri-list", 0, 0 },
	{ (gchar*) "_NETSCAPE_URL", 0, 1 }
};

63
struct _TotemGriloPrivate {
64
	Totem *totem;
65
	GtkWindow *main_window;
66

67
	gboolean plugins_activated;
68

69
	GrlSource *local_metadata_src;
70
	GrlSource *title_parsing_src;
71
	GrlSource *metadata_store_src;
72
	GrlSource *bookmarks_src;
73
	gboolean fs_plugin_configured;
74

75 76
	TotemGriloPage current_page;

77 78 79 80
	/* Current media selected in results*/
	GrlMedia *selected_media;

	/* Search related information */
81
	GrlSource *search_source;
82 83
	guint search_id;
	gint search_page;
84
	guint search_remaining;
85 86
	gchar *search_text;

Bastien Nocera's avatar
Bastien Nocera committed
87 88
	/* Toolbar widgets */
	GtkWidget *header;
89 90 91
	gboolean show_back_button;
	gboolean show_search_button;
	gboolean show_select_button;
92
	GMenuModel *selectmenu;
93 94
	GSimpleAction *select_all_action;
	GSimpleAction *select_none_action;
95 96

	/* Source switcher */
97
	GtkWidget *switcher;
98 99
	GtkWidget *recent, *channels, *search_hidden_button;
	char *last_page;
Bastien Nocera's avatar
Bastien Nocera committed
100

101 102
	/* Browser widgets */
	GtkWidget *browser;
103
	guint dnd_handler_id;
104
	GtkTreeModel *recent_model;
105
	GtkTreeModel *recent_sort_model;
106
	GtkTreeModel *browser_model;
107 108
	GtkTreeModel *browser_filter_model;
	gboolean in_search;
109
	GList *metadata_keys;
110
	guint thumbnail_update_id;
111 112

	/* Search widgets */
113
	GtkWidget *search_bar;
114 115
	GtkWidget *search_entry;
	GtkTreeModel *search_results_model;
116
	GHashTable *search_sources_ht;
117

118 119 120
	/* Selection toolbar */
	GtkWidget *selection_bar;
	GtkWidget *selection_revealer;
121 122

	GCancellable *thumbnail_cancellable;
123
};
124

125 126
enum {
	PROP_0,
127 128
	PROP_TOTEM,
	PROP_HEADER,
129 130
	PROP_SHOW_BACK_BUTTON,
	PROP_CURRENT_PAGE
131 132 133
};

G_DEFINE_TYPE_WITH_CODE (TotemGrilo, totem_grilo, GTK_TYPE_BOX,
134
                         G_ADD_PRIVATE (TotemGrilo));
135 136

typedef struct {
137
	TotemGrilo *totem_grilo;
138
	gboolean ignore_boxes; /* For the recent view */
139
	GtkTreeRowReference *ref_parent;
140
	GtkTreeModel *model;
141 142 143
} BrowseUserData;

typedef struct {
144
	TotemGrilo *totem_grilo;
145
	GrlMedia *media;
146
	GrlSource *source;
147
	GtkTreeModel *model;
148 149 150
	GtkTreeRowReference *reference;
} SetThumbnailData;

151 152
typedef struct {
	gboolean found;
153
	GrlKeyID key;
154 155 156 157
	GtkTreeIter *iter;
	GrlMedia *media;
} FindMediaData;

158 159 160 161 162
typedef struct {
	GtkTreeModel *model;
	gboolean all_removable;
} CanRemoveData;

163
enum {
164
	MODEL_RESULTS_SOURCE = GD_MAIN_COLUMN_LAST,
165 166 167 168
	MODEL_RESULTS_CONTENT,
	MODEL_RESULTS_IS_PRETHUMBNAIL,
	MODEL_RESULTS_PAGE,
	MODEL_RESULTS_REMAINING,
169 170
	MODEL_RESULTS_SORT_PRIORITY,
	MODEL_RESULTS_CAN_REMOVE
171 172
};

173 174 175 176 177 178
enum {
	CAN_REMOVE_UNSUPPORTED = -1,
	CAN_REMOVE_FALSE       = 0,
	CAN_REMOVE_TRUE        = 1
};

179
static gboolean
180 181
strv_has_prefix (const char * const *strv,
		 const char         *str)
182
{
183
	const char * const *s = strv;
184 185 186 187 188 189 190 191 192 193 194 195 196 197

	while (*s) {
		if (g_str_has_prefix (str, *s))
			return TRUE;
		s++;
	}

	return FALSE;
}

static gboolean
source_is_blacklisted (GrlSource *source)
{
	const char *id;
198
	const char * const sources[] = {
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
		"grl-shoutcast",
		"grl-flickr",
		"grl-podcasts",
		"grl-dmap",
		NULL
	};

	id = grl_source_get_id (source);
	g_assert (id);

	return strv_has_prefix (sources, id);
}

static gboolean
source_is_browse_blacklisted (GrlSource *source)
{
	const char *id;
216
	const char * const sources[] = {
217
		/* https://gitlab.gnome.org/GNOME/grilo/issues/36 */
218 219 220 221 222 223 224 225 226 227 228 229 230 231
		"grl-youtube",
		NULL
	};

	id = grl_source_get_id (source);
	g_assert (id);

	return strv_has_prefix (sources, id);
}

static gboolean
source_is_search_blacklisted (GrlSource *source)
{
	const char *id;
232
	const char * const sources[] = {
233 234 235 236 237 238 239 240 241 242 243 244 245 246
		"grl-metadata-store",
		NULL
	};

	id = grl_source_get_id (source);
	g_assert (id);

	return strv_has_prefix (sources, id);
}

static gboolean
source_is_recent (GrlSource *source)
{
	const char *id;
247
	const char * const sources[] = {
248 249
		"grl-tracker-source",
		"grl-optical-media",
250
		"grl-bookmarks",
251 252 253 254 255 256 257 258 259
		NULL
	};

	id = grl_source_get_id (source);
	g_assert (id);

	return strv_has_prefix (sources, id);
}

260
static gboolean
261
source_is_forbidden (GrlSource *source)
262 263 264 265
{
	const char **tags;

	tags = grl_source_get_tags (source);
266 267
	if (!tags)
		return FALSE;
268

269 270
	return strv_has_prefix (tags, "adult") ||
		strv_has_prefix (tags, "torrent");
271 272
}

273
static gchar *
274
get_secondary_text (GrlMedia *media)
275
{
276 277 278
	const char *artist;
	int duration;

279 280 281 282 283 284 285 286 287
	if (grl_data_get_string (GRL_DATA (media), GRL_METADATA_KEY_SHOW) != NULL) {
		int season, episode;

		season = grl_data_get_int (GRL_DATA (media), GRL_METADATA_KEY_SEASON);
		episode = grl_data_get_int (GRL_DATA (media), GRL_METADATA_KEY_EPISODE);
		if (season != 0 && episode != 0)
			return g_strdup_printf (_("Season %d Episode %d"), season, episode);
	}

288 289 290 291 292
	artist = grl_data_get_string (GRL_DATA (media), GRL_METADATA_KEY_ARTIST);
	if (artist != NULL)
		return g_strdup (artist);
	duration = grl_media_get_duration (media);
	if (duration > 0)
293
		return totem_time_to_string (duration * 1000, FALSE, FALSE);
294
	return NULL;
295 296
}

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
static const char *
get_primary_text (GrlMedia *media)
{
	const char *show;

	show = grl_data_get_string (GRL_DATA (media), GRL_METADATA_KEY_SHOW);
	if (show)
		return show;
	return grl_media_get_title (media);
}

static char *
get_title (GrlMedia *media)
{
	const char *show;

	show = grl_data_get_string (GRL_DATA (media), GRL_METADATA_KEY_SHOW);
	if (show != NULL) {
		int season, episode;

		season = grl_data_get_int (GRL_DATA (media), GRL_METADATA_KEY_SEASON);
		episode = grl_data_get_int (GRL_DATA (media), GRL_METADATA_KEY_EPISODE);
		if (season != 0 && episode != 0) {
			/* translators: The first item is the show name, for example:
			 * Boardwalk Empire (Season 1 Episode 1) */
			return g_strdup_printf (_("%s (Season %d Episode %d)"), show, season, episode);
		}
	}

	return g_strdup (grl_media_get_title (media));
}

329
static int
330 331 332 333 334
can_remove (GrlSource *source,
	    GrlMedia  *media)
{
	const char *url;
	char *scheme;
335
	int ret;
336

337 338
	if (g_strcmp0 (grl_source_get_id (source), "grl-bookmarks") == 0)
		return CAN_REMOVE_TRUE;
339
	if (!media)
340
		goto fallback;
341
	if (grl_media_is_container (media))
342
		return CAN_REMOVE_FALSE;
343 344
	url = grl_media_get_url (media);
	if (!url)
345
		return CAN_REMOVE_FALSE;
346 347

	scheme = g_uri_parse_scheme (url);
348
	ret = (g_strcmp0 (scheme, "file") == 0) ? CAN_REMOVE_TRUE : CAN_REMOVE_FALSE;
349 350
	g_free (scheme);

351 352 353 354 355 356 357 358
	if (ret == CAN_REMOVE_TRUE)
		return CAN_REMOVE_TRUE;

fallback:
	if (!(grl_source_supported_operations (source) & GRL_OP_REMOVE))
		return CAN_REMOVE_UNSUPPORTED;

	return CAN_REMOVE_FALSE;
359 360
}

361
static void
362 363 364
get_thumbnail_cb (GObject *source_object,
		  GAsyncResult *res,
		  gpointer user_data)
365 366 367
{
	GtkTreeIter iter;
	SetThumbnailData *thumb_data = (SetThumbnailData *) user_data;
368
	GtkTreePath *path;
369
	GdkPixbuf *thumbnail;
370
	const GdkPixbuf *fallback_thumbnail;
371 372
	GtkTreeModel *view_model;
	GError *error = NULL;
373

374
	thumbnail = totem_grilo_get_thumbnail_finish (source_object, res, &error);
375 376
	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		goto out;
377

378
	path = gtk_tree_row_reference_get_path (thumb_data->reference);
379 380
	if (!path)
		goto out;
381
	gtk_tree_model_get_iter (thumb_data->model, &iter, path);
382

383 384 385 386
	if (thumbnail == NULL) {
		if (thumb_data->media)
			fallback_thumbnail = totem_grilo_get_video_icon ();
		else
387
			fallback_thumbnail = totem_grilo_get_channel_icon ();
388 389
	}

390 391
	gtk_tree_store_set (GTK_TREE_STORE (thumb_data->model),
			    &iter,
392
			    GD_MAIN_COLUMN_ICON, thumbnail ? thumbnail : fallback_thumbnail,
393 394 395 396 397 398
			    -1);
	g_clear_object (&thumbnail);

	/* Can we find that thumbnail in the view model? */
	view_model = gd_main_view_get_model (GD_MAIN_VIEW (thumb_data->totem_grilo->priv->browser));
	if (GTK_IS_TREE_MODEL_FILTER (view_model)) {
399 400 401 402 403 404 405 406 407
		GtkTreePath *parent_path;
		parent_path = gtk_tree_model_filter_convert_child_path_to_path (GTK_TREE_MODEL_FILTER (view_model), path);
		gtk_tree_path_free (path);
		path = parent_path;
	} else if (GTK_IS_TREE_MODEL_SORT (view_model)) {
		GtkTreePath *parent_path;
		parent_path = gtk_tree_model_sort_convert_child_path_to_path (GTK_TREE_MODEL_SORT (view_model), path);
		gtk_tree_path_free (path);
		path = parent_path;
408
	}
409

410 411
	if (path != NULL && gtk_tree_model_get_iter (view_model, &iter, path))
		gtk_tree_model_row_changed (view_model, path, &iter);
412
	g_clear_pointer (&path, gtk_tree_path_free);
413

414 415 416
out:
	g_clear_error (&error);

417 418
	/* Free thumb data */
	g_object_unref (thumb_data->totem_grilo);
419 420
	g_clear_object (&thumb_data->media);
	g_clear_object (&thumb_data->source);
421
	g_object_unref (thumb_data->model);
422 423 424 425 426
	gtk_tree_row_reference_free (thumb_data->reference);
	g_slice_free (SetThumbnailData, thumb_data);
}

static void
427
set_thumbnail_async (TotemGrilo   *self,
428
		     GObject      *object,
429 430
		     GtkTreeModel *model,
		     GtkTreePath  *path)
431 432 433
{
	SetThumbnailData *thumb_data;

434
	/* Let's read the thumbnail stream and set the thumbnail */
435
	thumb_data = g_slice_new0 (SetThumbnailData);
436
	thumb_data->totem_grilo = g_object_ref (self);
437 438 439 440
	if (GRL_IS_SOURCE (object))
		thumb_data->source = GRL_SOURCE (g_object_ref (object));
	else
		thumb_data->media = GRL_MEDIA (g_object_ref (object));
441 442 443
	thumb_data->model = g_object_ref (model);
	thumb_data->reference = gtk_tree_row_reference_new (model, path);

444
	totem_grilo_get_thumbnail (object, self->priv->thumbnail_cancellable, get_thumbnail_cb, thumb_data);
445 446 447
}

static gboolean
448
update_search_thumbnails_idle (TotemGrilo *self)
449 450 451
{
	GtkTreePath *start_path;
	GtkTreePath *end_path;
452
	GrlSource *source;
453
	gboolean is_prethumbnail = FALSE;
454
	GtkTreeModel *view_model, *model;
455
	GtkIconView *icon_view;
456

457 458
	self->priv->thumbnail_update_id = 0;

459 460
	icon_view = GTK_ICON_VIEW (gd_main_view_get_generic_view (GD_MAIN_VIEW (self->priv->browser)));
	if (!gtk_icon_view_get_visible_range (icon_view, &start_path, &end_path)) {
461 462
		return FALSE;
	}
463

464
	view_model = gtk_icon_view_get_model (icon_view);
465 466
	if (GTK_IS_TREE_MODEL_FILTER (view_model))
		model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (view_model));
467 468
	else if (GTK_IS_TREE_MODEL_SORT (view_model))
		model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (view_model));
469 470 471 472 473 474 475 476
	else
		model = view_model;

	for (;
	     gtk_tree_path_compare (start_path, end_path) <= 0;
	     gtk_tree_path_next (start_path)) {
		GtkTreePath *path;
		GtkTreeIter iter;
477
		GrlMedia *media;
478 479 480 481

		if (GTK_IS_TREE_MODEL_FILTER (view_model)) {
			path = gtk_tree_model_filter_convert_path_to_child_path (GTK_TREE_MODEL_FILTER (view_model),
										 start_path);
482 483 484
		} else if (GTK_IS_TREE_MODEL_SORT (view_model)) {
			path = gtk_tree_model_sort_convert_path_to_child_path (GTK_TREE_MODEL_SORT (view_model),
									       start_path);
485 486 487
		} else {
			path = gtk_tree_path_copy (start_path);
		}
488

489 490 491 492
		if (gtk_tree_model_get_iter (model, &iter, path) == FALSE) {
			gtk_tree_path_free (path);
			break;
		}
493

494
		media = NULL;
495 496 497
		gtk_tree_model_get (model,
		                    &iter,
		                    MODEL_RESULTS_CONTENT, &media,
498
		                    MODEL_RESULTS_SOURCE, &source,
499 500
		                    MODEL_RESULTS_IS_PRETHUMBNAIL, &is_prethumbnail,
		                    -1);
501
		if ((media != NULL || source != NULL) && is_prethumbnail) {
502
			set_thumbnail_async (self, media ? G_OBJECT (media) : G_OBJECT (source), model, path);
503
			gtk_tree_store_set (GTK_TREE_STORE (model),
504
			                    &iter,
505
			                    MODEL_RESULTS_IS_PRETHUMBNAIL, FALSE,
506 507
			                    -1);
		}
508

509
		g_clear_object (&media);
510
		g_clear_object (&source);
511
	}
512 513
	gtk_tree_path_free (start_path);
	gtk_tree_path_free (end_path);
514 515 516 517 518

	return FALSE;
}

static void
519
update_search_thumbnails (TotemGrilo *self)
520
{
521 522 523
	if (self->priv->thumbnail_update_id > 0)
		return;
	self->priv->thumbnail_update_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) update_search_thumbnails_idle, self, NULL);
524
	g_source_set_name_by_id (self->priv->thumbnail_update_id, "[totem] update_search_thumbnails_idle");
525 526
}

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
static void
update_media (GtkTreeStore *model,
	      GtkTreeIter  *iter,
	      GrlSource    *source,
	      GrlMedia     *media)
{
	GdkPixbuf *thumbnail;
	gboolean thumbnailing;
	char *secondary;
	GDateTime *mtime;

	thumbnail = totem_grilo_get_icon (media, &thumbnailing);
	secondary = get_secondary_text (media);
	mtime = grl_media_get_modification_date (media);

	gtk_tree_store_set (GTK_TREE_STORE (model), iter,
			    MODEL_RESULTS_SOURCE, source,
			    MODEL_RESULTS_CONTENT, media,
			    GD_MAIN_COLUMN_ICON, thumbnail,
			    MODEL_RESULTS_IS_PRETHUMBNAIL, thumbnailing,
547
			    GD_MAIN_COLUMN_PRIMARY_TEXT, get_primary_text (media),
548 549 550 551 552 553 554 555
			    GD_MAIN_COLUMN_SECONDARY_TEXT, secondary,
			    GD_MAIN_COLUMN_MTIME, mtime ? g_date_time_to_unix (mtime) : 0,
			    -1);

	g_clear_object (&thumbnail);
	g_free (secondary);
}

556
static void
557
add_local_metadata (TotemGrilo *self,
558 559
		    GrlSource  *source,
		    GrlMedia   *media)
560 561 562
{
	GrlOperationOptions *options;

563 564
	/* This is very slow and sync, so don't run it
	 * for non-local media */
565
	if (!source_is_recent (source))
566 567
		return;

568 569 570 571 572 573 574 575 576 577 578 579 580
	/* Avoid trying to get metadata for web radios */
	if (source == self->priv->bookmarks_src) {
		char *scheme;

		scheme = g_uri_parse_scheme (grl_media_get_url (media));
		if (g_strcmp0 (scheme, "http") == 0 ||
		    g_strcmp0 (scheme, "https") == 0) {
			g_free (scheme);
			return;
		}
		g_free (scheme);
	}

581
	options = grl_operation_options_new (NULL);
582
	grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_NORMAL);
583 584 585 586 587
	grl_source_resolve_sync (self->priv->title_parsing_src,
				 media,
				 self->priv->metadata_keys,
				 options,
				 NULL);
588 589
	grl_source_resolve_sync (self->priv->local_metadata_src,
				 media,
590
				 self->priv->metadata_keys,
591 592
				 options,
				 NULL);
593 594 595 596 597
	grl_source_resolve_sync (self->priv->metadata_store_src,
				 media,
				 self->priv->metadata_keys,
				 options,
				 NULL);
598 599 600
	g_object_unref (options);
}

601 602 603 604 605 606 607 608 609 610 611
static int
get_source_priority (GrlSource *source)
{
	const char *id;

	if (source == NULL)
		return 0;

	id = grl_source_get_id (source);
	if (g_str_equal (id, "grl-optical-media"))
		return 100;
612 613
	if (g_str_equal (id, "grl-bookmarks"))
		return 75;
614 615
	if (g_str_equal (id, "grl-tracker-source"))
		return 50;
616 617 618
	if (g_str_has_prefix (id, "grl-upnp-") ||
	    g_str_has_prefix (id, "grl-dleyna-"))
		return 25;
619 620 621
	return 0;
}

622 623 624 625 626 627 628 629 630 631
static void
add_media_to_model (GtkTreeStore *model,
		    GtkTreeIter  *parent,
		    GrlSource    *source,
		    GrlMedia     *media)
{
	GdkPixbuf *thumbnail;
	gboolean thumbnailing;
	char *secondary;
	GDateTime *mtime;
632
	int prio;
633 634 635 636

	thumbnail = totem_grilo_get_icon (media, &thumbnailing);
	secondary = get_secondary_text (media);
	mtime = grl_media_get_modification_date (media);
637
	prio = get_source_priority (source);
638 639 640 641 642 643

	gtk_tree_store_insert_with_values (GTK_TREE_STORE (model), NULL, parent, -1,
					   MODEL_RESULTS_SOURCE, source,
					   MODEL_RESULTS_CONTENT, media,
					   GD_MAIN_COLUMN_ICON, thumbnail,
					   MODEL_RESULTS_IS_PRETHUMBNAIL, thumbnailing,
644
					   GD_MAIN_COLUMN_PRIMARY_TEXT, get_primary_text (media),
645 646
					   GD_MAIN_COLUMN_SECONDARY_TEXT, secondary,
					   GD_MAIN_COLUMN_MTIME, mtime ? g_date_time_to_unix (mtime) : 0,
647
					   MODEL_RESULTS_SORT_PRIORITY, prio,
648
					   MODEL_RESULTS_CAN_REMOVE, can_remove (source, media),
649 650 651 652 653 654
					   -1);

	g_clear_object (&thumbnail);
	g_free (secondary);
}

655
static void
656 657 658 659 660
browse_cb (GrlSource    *source,
           guint         browse_id,
           GrlMedia     *media,
           guint         remaining,
           gpointer      user_data,
661 662 663
           const GError *error)
{
	BrowseUserData *bud;
664
	TotemGrilo *self;
665 666
	GtkTreeIter parent;
	GtkWindow *window;
667
	guint remaining_expected;
668 669 670 671 672 673 674 675

	bud = (BrowseUserData *) user_data;
	self = bud->totem_grilo;

	if (error != NULL &&
	    g_error_matches (error,
	                     GRL_CORE_ERROR,
	                     GRL_CORE_ERROR_OPERATION_CANCELLED) == FALSE) {
676
		window = totem_object_get_main_window (self->priv->totem);
677 678 679 680
		totem_interface_error (_("Browse Error"), error->message, window);
	}

	if (media != NULL) {
681
		if (bud->ref_parent) {
682 683
			GtkTreePath *path;

684
			path = gtk_tree_row_reference_get_path (bud->ref_parent);
685 686 687 688 689
			if (!path ||
			    !gtk_tree_model_get_iter (bud->model, &parent, path)) {
				g_clear_pointer (&path, gtk_tree_path_free);
				return;
			}
690 691 692 693 694 695

			gtk_tree_model_get (bud->model, &parent,
					    MODEL_RESULTS_REMAINING, &remaining_expected,
					    -1);
			remaining_expected--;
			gtk_tree_store_set (GTK_TREE_STORE (bud->model), &parent,
696
					    MODEL_RESULTS_REMAINING, remaining_expected,
697 698 699
					    -1);
		}

700 701
		if (grl_media_is_image (media) ||
		    grl_media_is_audio (media)) {
702 703
			/* This isn't supposed to happen as we filter for videos */
			g_assert_not_reached ();
704
		}
705

706
		if (grl_media_is_container (media) && bud->ignore_boxes) {
707 708 709 710 711 712 713
			/* Ignore boxes for certain sources */
		} else {
			add_local_metadata (self, source, media);
			add_media_to_model (GTK_TREE_STORE (bud->model),
					    bud->ref_parent ? &parent : NULL,
					    source, media);
		}
714 715 716 717 718

		g_object_unref (media);
	}

	if (remaining == 0) {
719
		g_application_unmark_busy (g_application_get_default ());
720 721 722
		gtk_tree_row_reference_free (bud->ref_parent);
		g_object_unref (bud->totem_grilo);
		g_slice_free (BrowseUserData, bud);
723 724

		update_search_thumbnails (self);
725 726 727 728
	}
}

static void
729 730 731 732 733 734
browse (TotemGrilo   *self,
	GtkTreeModel *model,
        GtkTreePath  *path,
        GrlSource    *source,
        GrlMedia     *container,
        gint          page)
735
{
736 737 738 739 740
	BrowseUserData *bud;
	GrlOperationOptions *default_options;
	GrlCaps *caps;

	g_return_if_fail (source != NULL);
741
	g_return_if_fail (page >= 1 || page == -1);
742 743 744 745

	caps = grl_source_get_caps (source, GRL_OP_BROWSE);

	default_options = grl_operation_options_new (NULL);
746
	grl_operation_options_set_resolution_flags (default_options, BROWSE_FLAGS);
747 748 749 750
	if (page >= 1) {
		grl_operation_options_set_skip (default_options, (page - 1) * PAGE_SIZE);
		grl_operation_options_set_count (default_options, PAGE_SIZE);
	}
751 752
	if (grl_caps_get_type_filter (caps) & GRL_TYPE_FILTER_VIDEO)
		grl_operation_options_set_type_filter (default_options, GRL_TYPE_FILTER_VIDEO);
753 754
	if (grl_caps_is_key_range_filter (caps, GRL_METADATA_KEY_DURATION))
		grl_operation_options_set_key_range_filter (default_options,
755
							    GRL_METADATA_KEY_DURATION, MIN_DURATION, NULL,
756
							    NULL);
757

758
	bud = g_slice_new0 (BrowseUserData);
759
	bud->totem_grilo = g_object_ref (self);
760
	bud->ignore_boxes = source_is_recent (source);
761 762 763 764
	if (path)
		bud->ref_parent = gtk_tree_row_reference_new (model, path);
	bud->model = g_object_ref (model);

765
	g_application_mark_busy (g_application_get_default ());
766 767
	grl_source_browse (source,
			   container,
768
			   self->priv->metadata_keys,
769 770 771 772 773
			   default_options,
			   browse_cb,
			   bud);

	g_object_unref (default_options);
774 775 776
}

static void
777
play (TotemGrilo *self,
778 779 780
      GrlSource  *source,
      GrlMedia   *media,
      gboolean    resolve_url)
781 782
{
	const gchar *url;
783
	char *title;
784 785

	url = grl_media_get_url (media);
786 787 788 789 790 791
	if (!url)
		url = grl_media_get_external_url (media);
	if (!url) {
		g_warning ("Cannot find URL for %s (source: %s), please file a bug at https://bugzilla.gnome.org/",
			   grl_media_get_id (media),
			   grl_media_get_source (media));
792 793 794
		return;
	}

795
	totem_object_clear_playlist (self->priv->totem);
796
	title = get_title (media);
797
	totem_object_add_to_playlist (self->priv->totem, url,
798
				      title,
799
				      TRUE);
800
	g_free (title);
801 802 803
}

static void
804 805 806 807 808
search_cb (GrlSource    *source,
           guint         search_id,
           GrlMedia     *media,
           guint         remaining,
           gpointer      user_data,
809 810 811
           const GError *error)
{
	GtkWindow *window;
812
	TotemGrilo *self;
813

814
	self = TOTEM_GRILO (user_data);
815 816 817 818 819

	if (error != NULL &&
	    g_error_matches (error,
	                     GRL_CORE_ERROR,
	                     GRL_CORE_ERROR_OPERATION_CANCELLED) == FALSE) {
820
		window = totem_object_get_main_window (self->priv->totem);
821 822 823 824 825
		totem_interface_error (_("Search Error"), error->message, window);
	}

	if (media != NULL) {
		self->priv->search_remaining--;
826

827 828
		if (grl_media_is_image (media) ||
		    grl_media_is_audio (media)) {
829 830
			/* This isn't supposed to happen as we filter for videos */
			g_assert_not_reached ();
831 832
		}

833
		add_local_metadata (self, source, media);
834 835 836
		add_media_to_model (GTK_TREE_STORE (self->priv->search_results_model),
				    NULL, source, media);

837 838 839 840
		g_object_unref (media);
	}

	if (remaining == 0) {
841
		g_application_unmark_busy (g_application_get_default ());
842 843 844 845 846 847
		self->priv->search_id = 0;
		gtk_widget_set_sensitive (self->priv->search_entry, TRUE);
		update_search_thumbnails (self);
	}
}

848
static GrlOperationOptions *
849
get_search_options (TotemGrilo *self)
850
{
851
	GrlOperationOptions *default_options;
852
	GrlOperationOptions *supported_options;
853 854

	default_options = grl_operation_options_new (NULL);
855
	grl_operation_options_set_resolution_flags (default_options, BROWSE_FLAGS);
856
	grl_operation_options_set_skip (default_options, self->priv->search_page * PAGE_SIZE);
857
	grl_operation_options_set_count (default_options, PAGE_SIZE);
858
	grl_operation_options_set_type_filter (default_options, GRL_TYPE_FILTER_VIDEO);
859
	grl_operation_options_set_key_range_filter (default_options,
860
						    GRL_METADATA_KEY_DURATION, MIN_DURATION, NULL,
861
						    NULL);
862 863 864 865 866 867 868 869 870 871 872 873

	/* And now remove all the unsupported filters and options */
	grl_operation_options_obey_caps (default_options,
					 grl_source_get_caps (GRL_SOURCE (self->priv->search_source), GRL_OP_SEARCH),
					 &supported_options,
					 NULL);
	g_object_unref (default_options);

	return supported_options;
}

static void
874
search_more (TotemGrilo *self)
875 876 877 878
{
	GrlOperationOptions *search_options;

	search_options = get_search_options (self);
879

880 881 882
	gtk_widget_set_sensitive (self->priv->search_entry, FALSE);
	self->priv->search_page++;
	self->priv->search_remaining = PAGE_SIZE;
883 884 885

	g_application_mark_busy (g_application_get_default ());

886
	if (self->priv->search_source != NULL) {
887 888 889
		self->priv->search_id =
			grl_source_search (self->priv->search_source,
			                   self->priv->search_text,
890
			                   self->priv->metadata_keys,
891
			                   search_options,
892 893
			                   search_cb,
			                   self);
894
	} else {
895 896 897
		self->priv->search_id =
			grl_multiple_search (NULL,
			                     self->priv->search_text,
898
			                     self->priv->metadata_keys,
899
			                     search_options,
900 901
			                     search_cb,
			                     self);
902
	}
903
	g_object_unref (search_options);
904

905
	if (self->priv->search_id == 0)
906 907 908 909
		search_cb (self->priv->search_source, 0, NULL, 0, self, NULL);
}

static void
910 911 912
search (TotemGrilo  *self,
	GrlSource   *source,
	const gchar *text)
913
{
914
	gtk_tree_store_clear (GTK_TREE_STORE (self->priv->search_results_model));
915
//	g_hash_table_remove_all (self->priv->cache_thumbnails);
916 917 918 919 920
	gtk_widget_set_sensitive (self->priv->search_entry, FALSE);
	self->priv->search_source = source;
	g_free (self->priv->search_text);
	self->priv->search_text = g_strdup (text);
	self->priv->search_page = 0;
921 922
	gd_main_view_set_model (GD_MAIN_VIEW (self->priv->browser),
				self->priv->search_results_model);
923
	self->priv->browser_filter_model = NULL;
924 925 926 927
	search_more (self);
}

static void
928 929
search_entry_activate_cb (GtkEntry   *entry,
			  TotemGrilo *self)
930
{
931
	GrlRegistry *registry;
932 933
	const char *id;
	const char *text;
934
	GrlSource *source;
935

936
	g_object_set (self, "show-back-button", FALSE, NULL);
937

938 939
	id = totem_search_entry_get_selected_id (TOTEM_SEARCH_ENTRY (self->priv->search_entry));
	g_return_if_fail (id != NULL);
940 941
	registry = grl_registry_get_default ();
	source = grl_registry_lookup_source (registry, id);
942
	g_return_if_fail (source != NULL);
943

944 945
	text = totem_search_entry_get_text (TOTEM_SEARCH_ENTRY (self->priv->search_entry));
	g_return_if_fail (text != NULL);
946

947 948
	g_object_set (self->priv->header, "search-string", text, NULL);

949
	self->priv->in_search = TRUE;
950
	search (self, source, text);
951 952 953
}

static void
954 955
set_browser_filter_model_for_path (TotemGrilo    *self,
				   GtkTreePath   *path)
956
{
957 958 959 960
	GtkTreeIter iter;
	int can_remove = CAN_REMOVE_FALSE;
	char *text = NULL;

961 962 963
	g_clear_object (&self->priv->browser_filter_model);
	self->priv->browser_filter_model = gtk_tree_model_filter_new (self->priv->browser_model, path);

964 965
	gd_main_view_set_model (GD_MAIN_VIEW (self->priv->browser),
				self->priv->browser_filter_model);
Bastien Nocera's avatar
Bastien Nocera committed
966

967 968 969 970 971 972 973
	if (path != NULL && gtk_tree_model_get_iter (self->priv->browser_model, &iter, path)) {
		gtk_tree_model_get (self->priv->browser_model, &iter,
				    GD_MAIN_COLUMN_PRIMARY_TEXT, &text,
				    MODEL_RESULTS_CAN_REMOVE, &can_remove,
				    -1);
	}

974
	g_object_set (self, "show-back-button", path != NULL, NULL);
975
	if (path == NULL) {
976
		totem_main_toolbar_set_custom_title (TOTEM_MAIN_TOOLBAR (self->priv->header), self->priv->switcher);
977
	} else {
978
		totem_main_toolbar_set_custom_title (TOTEM_MAIN_TOOLBAR (self->priv->header), NULL);
979
		totem_main_toolbar_set_title (TOTEM_MAIN_TOOLBAR (self->priv->header), text);
980
	}
981 982 983 984

	totem_selection_toolbar_set_show_delete_button (TOTEM_SELECTION_TOOLBAR (self->priv->selection_bar),
							can_remove != CAN_REMOVE_UNSUPPORTED);
	g_free (text);
985 986 987
}

static void
988
browser_activated_cb (GdMainView  *view,
989
                      GtkTreePath *path,
990
                      gpointer     user_data)
991
{
992
	guint remaining;
993 994 995 996
	gint page;
	GtkTreeModel *model;
	GtkTreeIter iter;
	GrlMedia *content;
997
	GrlSource *source;
998
	TotemGrilo *self = TOTEM_GRILO (user_data);
999 1000
	GtkTreeIter real_model_iter;
	GtkTreePath *treepath;
1001

1002
	model = gd_main_view_get_model (GD_MAIN_VIEW (view));
1003 1004 1005 1006 1007 1008 1009 1010
	gtk_tree_model_get_iter (model, &iter, path);
	gtk_tree_model_get (model, &iter,
	                    MODEL_RESULTS_SOURCE, &source,
	                    MODEL_RESULTS_CONTENT, &content,
	                    MODEL_RESULTS_PAGE, &page,
	                    MODEL_RESULTS_REMAINING, &remaining,
	                    -1);

1011
	/* Activate an item */
1012
	if (content != NULL && grl_media_is_container (content) == FALSE) {
1013 1014 1015 1016
		play (self, source, content, TRUE);
		goto free_data;
	}

1017 1018 1019
	/* Clicked on a container */
	gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER (model),
							  &real_model_iter, &iter);
1020

1021 1022 1023 1024
	treepath = gtk_tree_model_get_path (self->priv->browser_model, &real_model_iter);
	set_browser_filter_model_for_path (self, treepath);

	/* We need to fill the model with browse data */
1025
	if (remaining == 0) {
1026
		gtk_tree_store_set (GTK_TREE_STORE (self->priv->browser_model), &real_model_iter,
1027 1028 1029
		                    MODEL_RESULTS_PAGE, ++page,
		                    MODEL_RESULTS_REMAINING, PAGE_SIZE,
		                    -1);
1030
		browse (self, self->priv->browser_model, treepath, source, content, page);
1031
	}
1032
	gtk_tree_path_free (treepath);
1033

1034 1035 1036
free_data:
	g_clear_object (&source);
	g_clear_object (&content);
1037 1038
}

1039
static void
1040 1041
search_entry_source_changed_cb (GObject    *object,
                                GParamSpec *pspec,
1042
                                TotemGrilo *self)
1043
{
1044
	/* FIXME: Do we actually want to do that? */
1045
	if (self->priv->search_id > 0) {
1046
		grl_operation_cancel (self->priv->search_id);
1047 1048
		self->priv->search_id = 0;
	}
1049
	gtk_tree_store_clear (GTK_TREE_STORE (self->priv->search_results_model));
1050 1051
}

1052
static void
1053
search_activated_cb (GdMainView  *view,
1054
                     GtkTreePath *path,
1055
                     gpointer     user_data)
1056 1057 1058
{
	GtkTreeModel *model;
	GtkTreeIter iter;
1059
	GrlSource *source;
1060 1061
	GrlMedia *content;

1062
	model = gd_main_view_get_model (view);
1063 1064 1065 1066 1067 1068
	gtk_tree_model_get_iter (model, &iter, path);
	gtk_tree_model_get (model, &iter,
	                    MODEL_RESULTS_SOURCE, &source,
	                    MODEL_RESULTS_CONTENT, &content,
	                    -1);

1069
	play (TOTEM_GRILO (user_data), source, content, TRUE);
1070

1071 1072
	g_clear_object (&source);
	g_clear_object (&content);
1073 1074
}

1075
static void
1076 1077
item_activated_cb (GdMainView  *view,
		   const char  *id,
1078
		   GtkTreePath *path,
1079
		   gpointer     user_data)
1080
{
1081
	TotemGrilo *self = TOTEM_GRILO (user_data);
1082 1083 1084
	GtkTreeModel *model;

	model = gd_main_view_get_model (view);
1085

1086
	if (model == self->priv->search_results_model) {
1087
		search_activated_cb (view, path, user_data);
1088 1089
	} else {
		totem_main_toolbar_set_search_mode (TOTEM_MAIN_TOOLBAR (self->priv->header), FALSE);
1090
		browser_activated_cb (view, path, user_data);
1091
	}
1092 1093
}

1094 1095 1096 1097 1098 1099 1100
static gboolean
find_media_cb (GtkTreeModel  *model,
	       GtkTreePath   *path,
	       GtkTreeIter   *iter,
	       FindMediaData *data)
{
	GrlMedia *media;
1101
	const char *a, *b;
1102 1103 1104 1105

	gtk_tree_model_get (model, iter,
			    MODEL_RESULTS_CONTENT, &media,
			    -1);
1106 1107 1108
	if (!media)
		return FALSE;

1109 1110 1111 1112
	a = grl_data_get_string (GRL_DATA (media), data->key);
	b = grl_data_get_string (GRL_DATA (data->media), data->key);

	if (g_strcmp0 (a, b) == 0) {
1113 1114
		g_object_unref (media);
		data->found = TRUE;
1115
		data->iter = gtk_tree_iter_copy (iter);
1116 1117 1118 1119 1120 1121 1122
		return TRUE;
	}
	g_object_unref (media);
	return FALSE;
}

static gboolean
1123 1124 1125
find_media (GtkTreeModel  *model,
	    GrlMedia      *media,
	    GtkTreeIter  **iter)
1126 1127 1128 1129
{
	FindMediaData data;

	data.found = FALSE;
1130
	data.key = GRL_METADATA_KEY_ID;
1131
	data.media = media;
1132
	data.iter = NULL;
1133 1134
	gtk_tree_model_foreach (model, (GtkTreeModelForeachFunc) find_media_cb, &data);

1135
	*iter = data.iter;
1136 1137 1138 1139 1140

	return data.found;
}

static GtkTreeModel *
1141
get_tree_model_for_source (TotemGrilo *self,
1142
			   GrlSource  *source)
1143
{
1144
	if (source_is_recent (source))
1145
		return self->priv->recent_model;
1146 1147 1148 1149 1150

	return self->priv->browser_model;
}

static void
1151
content_changed (TotemGrilo   *self,
1152 1153
		 GrlSource    *source,
		 GPtrArray    *changed_medias)
1154 1155 1156 1157 1158 1159 1160 1161
{
	GtkTreeModel *model;
	guint i;

	model = get_tree_model_for_source (self, source);

	for (i = 0; i < changed_medias->len; i++) {
		GrlMedia *media = changed_medias->pdata[i];
1162
		GtkTreeIter *iter;
1163 1164 1165 1166 1167
		char *str;

		str = grl_media_serialize (media);
		g_debug ("About to change %s in the store", str);
		g_free (str);
1168

1169 1170 1171
		if (find_media (model, media, &iter)) {
			update_media (GTK_TREE_STORE (model), iter, source, media);
			gtk_tree_iter_free (iter);
1172 1173 1174
		} else {
			g_debug ("Could not find '%s' to change in the store",
				 grl_media_get_id (media));
1175
		}
1176 1177 1178 1179
	}
}

static void
1180
content_removed (TotemGrilo   *self,
1181 1182
		 GrlSource    *source,
		 GPtrArray    *changed_medias)
1183 1184 1185 1186 1187 1188 1189 1190
{
	GtkTreeModel *model;
	guint i;

	model = get_tree_model_for_source (self, source);

	for (i = 0; i < changed_medias->len; i++) {
		GrlMedia *media = changed_medias->pdata[i];
1191 1192
		GtkTreeIter *iter;
		char *str;
1193

1194 1195 1196 1197
		str = grl_media_serialize (media);
		g_debug ("About to remove %s from the store", str);
		g_free (str);

1198 1199 1200
		if (find_media (model, media, &iter)) {
			gtk_tree_store_remove (GTK_TREE_STORE (model), iter);
			gtk_tree_iter_free (iter);
1201
		} else {
1202
			g_debug ("Could not find '%s' to remove in the store",
1203
				 grl_media_get_id (media));
1204
		}
1205 1206 1207 1208
	}
}

static void
1209
content_added (TotemGrilo   *self,
1210 1211
	       GrlSource    *source,
	       GPtrArray    *changed_medias)
1212 1213 1214 1215 1216 1217
{
	GtkTreeModel *model;
	guint i;

	model = get_tree_model_for_source (self, source);
	/* We're missing a container for the new media */
1218
	if (model != self->priv->recent_model)
1219 1220 1221 1222
		return;

	for (i = 0; i < changed_medias->len; i++) {
		GrlMedia *media = changed_medias->pdata[i];
1223 1224 1225 1226 1227
		char *str;

		str = grl_media_serialize (media);
		g_debug ("About to add %s to the store", str);
		g_free (str);
1228

1229
		add_local_metadata (self, source, media);
1230 1231 1232 1233 1234
		add_media_to_model (GTK_TREE_STORE (model), NULL, source, media);
	}
}

static void
1235 1236 1237 1238 1239
content_changed_cb (GrlSource           *source,
		    GPtrArray           *changed_medias,
		    GrlSourceChangeType  change_type,
		    gboolean             location_unknown,
		    TotemGrilo          *self)
1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257
{
	switch (change_type) {
	case GRL_CONTENT_CHANGED:
		content_changed (self, source, changed_medias);
		break;
	case GRL_CONTENT_ADDED:
		/* Added somewhere we don't know?
		 * We'll see it again when we browse away and back again */
		if (location_unknown)
			return;
		content_added (self, source, changed_medias);
		break;
	case GRL_CONTENT_REMOVED:
		content_removed (self, source, changed_medias);
		break;
	}
}

1258
static void
1259
source_added_cb (GrlRegistry *registry,
1260 1261
                 GrlSource   *source,
                 gpointer     user_data)
1262 1263
{
	const gchar *name;
1264
	TotemGrilo *self;
1265
	GrlSupportedOps ops;
1266
	const char *id;
1267

1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283
	self = TOTEM_GRILO (user_data);
	id = grl_source_get_id (source);

	/* Metadata */
	if (g_str_equal (id, "grl-video-title-parsing"))
		self->priv->title_parsing_src = source;
	else if (g_str_equal (id, "grl-local-metadata"))
		self->priv->local_metadata_src = source;
	else if (g_str_equal (id, "grl-metadata-store"))
		self->priv->metadata_store_src = source;
	else if (g_str_equal (id, "grl-bookmarks"))
		self->priv->bookmarks_src = source;

	if (self->priv->plugins_activated == FALSE)
		return;

1284
	if (source_is_blacklisted (source) ||
1285
	    source_is_forbidden (source) ||
1286
	    !(grl_source_get_supported_media (source) & GRL_MEDIA_TYPE_VIDEO)) {
1287 1288 1289
		grl_registry_unregister_source (registry,
		                                source,
		                                NULL);
1290 1291 1292
		return;
	}

1293 1294 1295 1296 1297 1298 1299
	/* The filesystem plugin */
	if (g_str_equal (id, "grl-filesystem") &&
	    self->priv->fs_plugin_configured == FALSE) {
		return;
	}

	/* The local search source */
1300 1301 1302