1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /* totem-playlist.c
3 
4    Copyright (C) 2002, 2003, 2004, 2005 Bastien Nocera
5 
6    The Gnome Library is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Library General Public License as
8    published by the Free Software Foundation; either version 2 of the
9    License, or (at your option) any later version.
10 
11    The Gnome Library is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14    Library General Public License for more details.
15 
16    You should have received a copy of the GNU Library General Public
17    License along with the Gnome Library; see the file COPYING.LIB.  If not,
18    write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19    Boston, MA 02110-1301  USA.
20 
21    Author: Bastien Nocera <hadess@hadess.net>
22  */
23 
24 #include "config.h"
25 #include "totem-playlist.h"
26 
27 #include <stdlib.h>
28 #include <string.h>
29 #include <gtk/gtk.h>
30 #include <glib/gi18n.h>
31 #include <gdk/gdkkeysyms.h>
32 #include <gio/gio.h>
33 
34 #include "totem-uri.h"
35 #include "totem-interface.h"
36 
37 #define PL_LEN (gtk_tree_model_iter_n_children (playlist->priv->model, NULL))
38 
39 static gboolean totem_playlist_add_one_mrl (TotemPlaylist *playlist,
40 					    const char    *mrl,
41 					    const char    *display_name,
42 					    const char    *content_type,
43 					    const char    *subtitle_uri,
44 					    gint64         starttime,
45 					    gboolean       playing);
46 
47 typedef gboolean (*ClearComparisonFunc) (TotemPlaylist *playlist, GtkTreeIter *iter, gconstpointer data);
48 
49 static void totem_playlist_clear_with_compare (TotemPlaylist *playlist,
50 					       ClearComparisonFunc func,
51 					       gconstpointer data);
52 
53 /* Callback function for GtkBuilder */
54 G_MODULE_EXPORT void totem_playlist_add_files (GtkWidget *widget, TotemPlaylist *playlist);
55 G_MODULE_EXPORT void playlist_remove_button_clicked (GtkWidget *button, TotemPlaylist *playlist);
56 G_MODULE_EXPORT void playlist_copy_location_action_callback (GtkWidget *button, TotemPlaylist *playlist);
57 G_MODULE_EXPORT void playlist_select_subtitle_action_callback (GtkWidget *button, TotemPlaylist *playlist);
58 G_MODULE_EXPORT void print_metadata_action_callback (GtkWidget *button, TotemPlaylist *playlist);
59 
60 
61 typedef struct {
62 	TotemPlaylist *playlist;
63 	TotemPlaylistForeachFunc callback;
64 	gpointer user_data;
65 } PlaylistForeachContext;
66 
67 struct TotemPlaylistPrivate
68 {
69 	GtkWidget *treeview;
70 	GtkTreeModel *model;
71 	GtkTreePath *current;
72 	GtkTreeSelection *selection;
73 	TotemPlParser *parser;
74 
75 	/* Widgets */
76 	GtkWidget *remove_button;
77 
78 	GSettings *settings;
79 	GSettings *lockdown_settings;
80 
81 	/* This is a scratch list for when we're removing files */
82 	GList *list;
83 	guint current_to_be_removed : 1;
84 
85 	guint disable_save_to_disk : 1;
86 
87 	/* Repeat mode */
88 	guint repeat : 1;
89 };
90 
91 /* Signals */
92 enum {
93 	CHANGED,
94 	ITEM_ACTIVATED,
95 	ACTIVE_NAME_CHANGED,
96 	CURRENT_REMOVED,
97 	SUBTITLE_CHANGED,
98 	ITEM_ADDED,
99 	ITEM_REMOVED,
100 	LAST_SIGNAL
101 };
102 
103 enum {
104 	PLAYING_COL,
105 	FILENAME_COL,
106 	FILENAME_ESCAPED_COL,
107 	URI_COL,
108 	TITLE_CUSTOM_COL,
109 	SUBTITLE_URI_COL,
110 	FILE_MONITOR_COL,
111 	MOUNT_COL,
112 	MIME_TYPE_COL,
113 	STARTTIME_COL,
114 	NUM_COLS
115 };
116 
117 enum {
118 	PROP_0,
119 	PROP_REPEAT
120 };
121 
122 typedef struct {
123 	const char *name;
124 	const char *suffix;
125 	TotemPlParserType type;
126 } PlaylistSaveType;
127 
128 static int totem_playlist_table_signals[LAST_SIGNAL];
129 
130 static void init_treeview (GtkWidget *treeview, TotemPlaylist *playlist);
131 
132 #define totem_playlist_unset_playing(x) totem_playlist_set_playing(x, TOTEM_PLAYLIST_STATUS_NONE)
133 
G_DEFINE_TYPE_WITH_PRIVATE(TotemPlaylist,totem_playlist,GTK_TYPE_BOX)134 G_DEFINE_TYPE_WITH_PRIVATE (TotemPlaylist, totem_playlist, GTK_TYPE_BOX)
135 
136 /* Helper functions */
137 void
138 totem_playlist_select_subtitle_dialog(TotemPlaylist *playlist, TotemPlaylistSelectDialog mode)
139 {
140 	char *subtitle, *current, *uri;
141 	GFile *file, *dir;
142 	TotemPlaylistStatus playing;
143 	GtkTreeIter iter;
144 
145 	if (mode == TOTEM_PLAYLIST_DIALOG_PLAYING) {
146 		/* Set subtitle file for the currently playing movie */
147 		gtk_tree_model_get_iter (playlist->priv->model, &iter, playlist->priv->current);
148 	} else if (mode == TOTEM_PLAYLIST_DIALOG_SELECTED) {
149 		/* Set subtitle file in for the first selected playlist item */
150 		GList *l;
151 
152 		l = gtk_tree_selection_get_selected_rows (playlist->priv->selection, NULL);
153 		if (l == NULL)
154 			return;
155 		gtk_tree_model_get_iter (playlist->priv->model, &iter, l->data);
156 		g_list_free_full (l, (GDestroyNotify) gtk_tree_path_free);
157 	} else {
158 		g_assert_not_reached ();
159 	}
160 
161 	/* Look for the directory of the current movie */
162 	gtk_tree_model_get (playlist->priv->model, &iter,
163 			    URI_COL, &current,
164 			    -1);
165 
166 	if (current == NULL)
167 		return;
168 
169 	uri = NULL;
170 	file = g_file_new_for_uri (current);
171 	dir = g_file_get_parent (file);
172 	g_object_unref (file);
173 	if (dir != NULL) {
174 		uri = g_file_get_uri (dir);
175 		g_object_unref (dir);
176 	}
177 
178 	subtitle = totem_add_subtitle (NULL, uri);
179 	g_free (uri);
180 
181 	if (subtitle == NULL)
182 		return;
183 
184 	gtk_tree_model_get (playlist->priv->model, &iter,
185 			    PLAYING_COL, &playing,
186 			    -1);
187 
188 	gtk_list_store_set (GTK_LIST_STORE(playlist->priv->model), &iter,
189 			    SUBTITLE_URI_COL, subtitle,
190 			    -1);
191 
192 	if (playing != TOTEM_PLAYLIST_STATUS_NONE) {
193 		g_signal_emit (G_OBJECT (playlist),
194 			       totem_playlist_table_signals[SUBTITLE_CHANGED], 0,
195 			       NULL);
196 	}
197 
198 	g_free(subtitle);
199 }
200 
201 void
totem_playlist_set_current_subtitle(TotemPlaylist * playlist,const char * subtitle_uri)202 totem_playlist_set_current_subtitle (TotemPlaylist *playlist, const char *subtitle_uri)
203 {
204 	GtkTreeIter iter;
205 
206 	if (playlist->priv->current == NULL)
207 		return;
208 
209 	gtk_tree_model_get_iter (playlist->priv->model, &iter, playlist->priv->current);
210 
211 	gtk_list_store_set (GTK_LIST_STORE(playlist->priv->model), &iter,
212 			    SUBTITLE_URI_COL, subtitle_uri,
213 			    -1);
214 
215 	g_signal_emit (G_OBJECT (playlist),
216 		       totem_playlist_table_signals[SUBTITLE_CHANGED], 0,
217 		       NULL);
218 }
219 
220 /* This one returns a new string, in UTF8 even if the MRL is encoded
221  * in the locale's encoding
222  */
223 static char *
totem_playlist_mrl_to_title(const gchar * mrl)224 totem_playlist_mrl_to_title (const gchar *mrl)
225 {
226 	GFile *file;
227 	char *filename_for_display, *unescaped;
228 
229 	if (g_str_has_prefix (mrl, "dvd://") != FALSE) {
230 		/* This is "Title 3", where title is a DVD title
231 		 * Note: NOT a DVD chapter */
232 		return g_strdup_printf (_("Title %d"), (int) g_strtod (mrl + 6, NULL));
233 	} else if (g_str_has_prefix (mrl, "dvb://") != FALSE) {
234 		/* This is "BBC ONE(BBC)" for "dvb://BBC ONE(BBC)" */
235 		return g_strdup (mrl + 6);
236 	}
237 
238 	file = g_file_new_for_uri (mrl);
239 	unescaped = g_file_get_basename (file);
240 	g_object_unref (file);
241 
242 	filename_for_display = g_filename_to_utf8 (unescaped,
243 			-1,             /* length */
244 			NULL,           /* bytes_read */
245 			NULL,           /* bytes_written */
246 			NULL);          /* error */
247 
248 	if (filename_for_display == NULL)
249 	{
250 		filename_for_display = g_locale_to_utf8 (unescaped,
251 				-1, NULL, NULL, NULL);
252 		if (filename_for_display == NULL) {
253 			filename_for_display = g_filename_display_name
254 				(unescaped);
255 		}
256 		g_free (unescaped);
257 		return filename_for_display;
258 	}
259 
260 	g_free (unescaped);
261 
262 	return filename_for_display;
263 }
264 
265 static gboolean
totem_playlist_save_iter_foreach(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer user_data)266 totem_playlist_save_iter_foreach (GtkTreeModel *model,
267 				  GtkTreePath  *path,
268 				  GtkTreeIter  *iter,
269 				  gpointer      user_data)
270 {
271 	TotemPlPlaylist *playlist = user_data;
272 	TotemPlPlaylistIter pl_iter;
273 	gchar *uri, *title, *subtitle_uri, *mime_type, *starttime_str;
274 	TotemPlaylistStatus status;
275 	gboolean custom_title;
276 	gint64 starttime;
277 
278 	gtk_tree_model_get (model, iter,
279 			    URI_COL, &uri,
280 			    FILENAME_COL, &title,
281 			    TITLE_CUSTOM_COL, &custom_title,
282 			    SUBTITLE_URI_COL, &subtitle_uri,
283 			    PLAYING_COL, &status,
284 			    MIME_TYPE_COL, &mime_type,
285 			    STARTTIME_COL, &starttime,
286 			    -1);
287 
288 	/* Prefer the current position for the starttime, if one is passed */
289 	starttime_str = NULL;
290 	if (status != TOTEM_PLAYLIST_STATUS_NONE) {
291 		gint64 new_starttime;
292 
293 		new_starttime = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (playlist), "starttime"));
294 		if (new_starttime != 0)
295 			starttime = new_starttime;
296 	}
297 	if (starttime != 0)
298 		starttime_str = g_strdup_printf ("%" G_GINT64_FORMAT, starttime);
299 
300 	totem_pl_playlist_append (playlist, &pl_iter);
301 	totem_pl_playlist_set (playlist, &pl_iter,
302 			       TOTEM_PL_PARSER_FIELD_URI, uri,
303 			       TOTEM_PL_PARSER_FIELD_TITLE, (custom_title) ? title : NULL,
304 			       TOTEM_PL_PARSER_FIELD_SUBTITLE_URI, subtitle_uri,
305 			       TOTEM_PL_PARSER_FIELD_PLAYING, status != TOTEM_PLAYLIST_STATUS_NONE ? "true" : "",
306 			       TOTEM_PL_PARSER_FIELD_CONTENT_TYPE, mime_type,
307 			       TOTEM_PL_PARSER_FIELD_STARTTIME, starttime_str,
308 			       NULL);
309 
310 	g_free (uri);
311 	g_free (title);
312 	g_free (subtitle_uri);
313 	g_free (mime_type);
314 	g_free (starttime_str);
315 
316 	return FALSE;
317 }
318 
319 static void
totem_playlist_save_session_playlist_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)320 totem_playlist_save_session_playlist_cb (GObject       *source_object,
321 					 GAsyncResult  *res,
322 					 gpointer       user_data)
323 {
324 	g_autoptr(GError) error = NULL;
325 	gboolean ret;
326 
327 	ret = totem_pl_parser_save_finish (TOTEM_PL_PARSER (source_object),
328 					   res, &error);
329 	if (!ret && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
330 		g_warning ("Failed to save the session playlist: %s", error->message);
331 }
332 
333 static void
totem_playlist_delete_session_playlist_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)334 totem_playlist_delete_session_playlist_cb (GObject       *source_object,
335 					   GAsyncResult  *res,
336 					   gpointer       user_data)
337 {
338 	g_autoptr(GError) error = NULL;
339 	gboolean ret;
340 
341 	ret = g_file_delete_finish (G_FILE (source_object), res, &error);
342 	if (!ret) {
343 		if(!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
344 		   !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
345 		g_warning ("Failed to delete session playlist: %s", error->message);
346 	}
347 }
348 
349 void
totem_playlist_save_session_playlist(TotemPlaylist * playlist,GFile * output,gint64 starttime)350 totem_playlist_save_session_playlist (TotemPlaylist *playlist,
351 				      GFile         *output,
352 				      gint64         starttime)
353 {
354 	g_autoptr(TotemPlPlaylist) pl_playlist = NULL;
355 
356 	if (playlist->priv->disable_save_to_disk) {
357 		/* On lockdown, we do not touch the disk,
358 		 * even to remove the existing session */
359 		return;
360 	}
361 	if (PL_LEN == 0) {
362 		g_file_delete_async (output, 0, NULL, totem_playlist_delete_session_playlist_cb, NULL);
363 		return;
364 	}
365 
366 	pl_playlist = totem_pl_playlist_new ();
367 
368 	if (starttime > 0)
369 		g_object_set_data (G_OBJECT (pl_playlist), "starttime", GINT_TO_POINTER (starttime));
370 
371 	gtk_tree_model_foreach (playlist->priv->model,
372 				totem_playlist_save_iter_foreach,
373 				pl_playlist);
374 
375 	totem_pl_parser_save_async (playlist->priv->parser,
376 				    pl_playlist,
377 				    output,
378 				    NULL,
379 				    TOTEM_PL_PARSER_XSPF,
380 				    NULL,
381 				    totem_playlist_save_session_playlist_cb,
382 				    NULL);
383 }
384 
385 static void
gtk_tree_selection_has_selected_foreach(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer user_data)386 gtk_tree_selection_has_selected_foreach (GtkTreeModel *model,
387 		GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
388 {
389 	int *retval = (gboolean *)user_data;
390 	*retval = TRUE;
391 }
392 
393 static gboolean
gtk_tree_selection_has_selected(GtkTreeSelection * selection)394 gtk_tree_selection_has_selected (GtkTreeSelection *selection)
395 {
396 	int retval, *boolean;
397 
398 	retval = FALSE;
399 	boolean = &retval;
400 	gtk_tree_selection_selected_foreach (selection,
401 			gtk_tree_selection_has_selected_foreach,
402 			(gpointer) (boolean));
403 
404 	return retval;
405 }
406 
407 void
print_metadata_action_callback(GtkWidget * button,TotemPlaylist * playlist)408 print_metadata_action_callback (GtkWidget *button, TotemPlaylist *playlist)
409 {
410 	GList *rows, *l;
411 	guint i;
412 
413 	rows = gtk_tree_selection_get_selected_rows (playlist->priv->selection, NULL);
414 	if (rows == NULL)
415 		return;
416 
417 	i = 0;
418 	for (l = rows; l != NULL; l = l->next) {
419 		g_autofree char *url = NULL;
420 		g_autofree char *sub_url = NULL;
421 		gboolean playing;
422 		GtkTreeIter iter;
423 
424 		gtk_tree_model_get_iter (playlist->priv->model, &iter, l->data);
425 		gtk_tree_model_get (playlist->priv->model,
426 				    &iter,
427 				    PLAYING_COL, &playing,
428 				    URI_COL, &url,
429 				    SUBTITLE_URI_COL, &sub_url,
430 				    -1);
431 
432 		g_print ("Item #%d\n", i);
433 		if (playing)
434 			g_print ("\tPlaying\n");
435 		g_print ("\tURI: %s\n", url);
436 		if (sub_url)
437 			g_print ("\tSubtitle URI: %s\n", sub_url);
438 
439 		gtk_tree_path_free (l->data);
440 		i++;
441 	}
442 
443 	g_list_free (rows);
444 }
445 
446 void
playlist_select_subtitle_action_callback(GtkWidget * button,TotemPlaylist * playlist)447 playlist_select_subtitle_action_callback (GtkWidget *button, TotemPlaylist *playlist)
448 {
449 	totem_playlist_select_subtitle_dialog (playlist, TOTEM_PLAYLIST_DIALOG_SELECTED);
450 }
451 
452 void
playlist_copy_location_action_callback(GtkWidget * button,TotemPlaylist * playlist)453 playlist_copy_location_action_callback (GtkWidget *button, TotemPlaylist *playlist)
454 {
455 	GList *l;
456 	GtkClipboard *clip;
457 	char *url;
458 	GtkTreeIter iter;
459 
460 	l = gtk_tree_selection_get_selected_rows (playlist->priv->selection, NULL);
461 	if (l == NULL)
462 		return;
463 
464 	gtk_tree_model_get_iter (playlist->priv->model, &iter, l->data);
465 	g_list_free_full (l, (GDestroyNotify) gtk_tree_path_free);
466 
467 	gtk_tree_model_get (playlist->priv->model,
468 			&iter,
469 			URI_COL, &url,
470 			-1);
471 
472 	/* Set both the middle-click and the super-paste buffers */
473 	clip = gtk_clipboard_get_for_display
474 		(gdk_display_get_default(), GDK_SELECTION_CLIPBOARD);
475 	gtk_clipboard_set_text (clip, url, -1);
476 	clip = gtk_clipboard_get_for_display
477 		(gdk_display_get_default(), GDK_SELECTION_PRIMARY);
478 	gtk_clipboard_set_text (clip, url, -1);
479 	g_free (url);
480 
481 }
482 
483 static void
selection_changed(GtkTreeSelection * treeselection,TotemPlaylist * playlist)484 selection_changed (GtkTreeSelection *treeselection, TotemPlaylist *playlist)
485 {
486 	gboolean sensitivity;
487 
488 	if (gtk_tree_selection_has_selected (treeselection))
489 		sensitivity = TRUE;
490 	else
491 		sensitivity = FALSE;
492 
493 	gtk_widget_set_sensitive (playlist->priv->remove_button, sensitivity);
494 }
495 
496 /* This function checks if the current item is NULL, and try to update it
497  * as the first item of the playlist if so. It returns TRUE if there is a
498  * current item */
499 static gboolean
update_current_from_playlist(TotemPlaylist * playlist)500 update_current_from_playlist (TotemPlaylist *playlist)
501 {
502 	int indice;
503 
504 	if (playlist->priv->current != NULL)
505 		return TRUE;
506 
507 	if (PL_LEN != 0)
508 	{
509 		indice = 0;
510 		playlist->priv->current = gtk_tree_path_new_from_indices (indice, -1);
511 	} else {
512 		return FALSE;
513 	}
514 
515 	return TRUE;
516 }
517 
518 void
totem_playlist_add_files(GtkWidget * widget,TotemPlaylist * playlist)519 totem_playlist_add_files (GtkWidget *widget, TotemPlaylist *playlist)
520 {
521 	GSList *filenames, *l;
522 	GList *mrl_list = NULL;
523 
524 	filenames = totem_add_files (NULL, NULL);
525 	if (filenames == NULL)
526 		return;
527 
528 	for (l = filenames; l != NULL; l = l->next) {
529 		char *mrl = l->data;
530 		mrl_list = g_list_prepend (mrl_list, totem_playlist_mrl_data_new (mrl, NULL));
531 		g_free (mrl);
532 	}
533 
534 	g_slist_free (filenames);
535 
536 	if (mrl_list != NULL)
537 		totem_playlist_add_mrls (playlist, g_list_reverse (mrl_list), TRUE, NULL, NULL, NULL);
538 }
539 
540 static void
totem_playlist_foreach_selected(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)541 totem_playlist_foreach_selected (GtkTreeModel *model, GtkTreePath *path,
542 		GtkTreeIter *iter, gpointer data)
543 {
544 	TotemPlaylist *playlist = (TotemPlaylist *)data;
545 	GtkTreeRowReference *ref;
546 
547 	/* We can't use gtk_list_store_remove() here
548 	 * So we build a list a RowReferences */
549 	ref = gtk_tree_row_reference_new (playlist->priv->model, path);
550 	playlist->priv->list = g_list_prepend
551 		(playlist->priv->list, (gpointer) ref);
552 	if (playlist->priv->current_to_be_removed == FALSE
553 	    && playlist->priv->current != NULL
554 	    && gtk_tree_path_compare (path, playlist->priv->current) == 0)
555 		playlist->priv->current_to_be_removed = TRUE;
556 }
557 
558 static void
totem_playlist_emit_item_removed(TotemPlaylist * playlist,GtkTreeIter * iter)559 totem_playlist_emit_item_removed (TotemPlaylist *playlist,
560 				  GtkTreeIter   *iter)
561 {
562 	gchar *filename = NULL;
563 	gchar *uri = NULL;
564 
565 	gtk_tree_model_get (playlist->priv->model, iter,
566 			    URI_COL, &uri, FILENAME_COL, &filename, -1);
567 
568 	g_signal_emit (playlist,
569 		       totem_playlist_table_signals[ITEM_REMOVED],
570 		       0, filename, uri);
571 
572 	g_free (filename);
573 	g_free (uri);
574 }
575 
576 static void
playlist_remove_files(TotemPlaylist * playlist)577 playlist_remove_files (TotemPlaylist *playlist)
578 {
579 	totem_playlist_clear_with_compare (playlist, NULL, NULL);
580 }
581 
582 void
playlist_remove_button_clicked(GtkWidget * button,TotemPlaylist * playlist)583 playlist_remove_button_clicked (GtkWidget *button, TotemPlaylist *playlist)
584 {
585 	playlist_remove_files (playlist);
586 }
587 
588 static int
totem_playlist_key_press(GtkWidget * win,GdkEventKey * event,TotemPlaylist * playlist)589 totem_playlist_key_press (GtkWidget *win, GdkEventKey *event, TotemPlaylist *playlist)
590 {
591 	/* Special case some shortcuts */
592 	if (event->state != 0) {
593 		if ((event->state & GDK_CONTROL_MASK)
594 		    && event->keyval == GDK_KEY_a) {
595 			gtk_tree_selection_select_all
596 				(playlist->priv->selection);
597 			return TRUE;
598 		}
599 	}
600 
601 	/* If we have modifiers, and either Ctrl, Mod1 (Alt), or any
602 	 * of Mod3 to Mod5 (Mod2 is num-lock...) are pressed, we
603 	 * let Gtk+ handle the key */
604 	if (event->state != 0
605 			&& ((event->state & GDK_CONTROL_MASK)
606 			|| (event->state & GDK_MOD1_MASK)
607 			|| (event->state & GDK_MOD3_MASK)
608 			|| (event->state & GDK_MOD4_MASK)
609 			|| (event->state & GDK_MOD5_MASK)))
610 		return FALSE;
611 
612 	if (event->keyval == GDK_KEY_Delete)
613 	{
614 		playlist_remove_files (playlist);
615 		return TRUE;
616 	}
617 
618 	return FALSE;
619 }
620 
621 static void
set_playing_icon(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,TotemPlaylist * playlist)622 set_playing_icon (GtkTreeViewColumn *column, GtkCellRenderer *renderer,
623 		  GtkTreeModel *model, GtkTreeIter *iter, TotemPlaylist *playlist)
624 {
625 	TotemPlaylistStatus playing;
626 	const char *icon_name;
627 
628 	gtk_tree_model_get (model, iter, PLAYING_COL, &playing, -1);
629 
630 	switch (playing) {
631 		case TOTEM_PLAYLIST_STATUS_PLAYING:
632 			icon_name = "media-playback-start-symbolic";
633 			break;
634 		case TOTEM_PLAYLIST_STATUS_PAUSED:
635 			icon_name = "media-playback-pause-symbolic";
636 			break;
637 		case TOTEM_PLAYLIST_STATUS_NONE:
638 		default:
639 			icon_name = NULL;
640 	}
641 
642 	g_object_set (renderer, "icon-name", icon_name, NULL);
643 }
644 
645 static void
init_columns(GtkTreeView * treeview,TotemPlaylist * playlist)646 init_columns (GtkTreeView *treeview, TotemPlaylist *playlist)
647 {
648 	GtkCellRenderer *renderer;
649 	GtkTreeViewColumn *column;
650 
651 	/* Playing pix */
652 	renderer = gtk_cell_renderer_pixbuf_new ();
653 	column = gtk_tree_view_column_new ();
654 	g_object_set (G_OBJECT (column), "title", "Playlist", NULL);
655 	gtk_tree_view_column_pack_start (column, renderer, FALSE);
656 	gtk_tree_view_column_set_cell_data_func (column, renderer,
657 			(GtkTreeCellDataFunc) set_playing_icon, playlist, NULL);
658 	g_object_set (renderer, "stock-size", GTK_ICON_SIZE_MENU, NULL);
659 	gtk_tree_view_append_column (treeview, column);
660 
661 	/* Labels */
662 	renderer = gtk_cell_renderer_text_new ();
663 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
664 	gtk_tree_view_column_set_attributes (column, renderer,
665 			"text", FILENAME_COL, NULL);
666 }
667 
668 static void
treeview_row_changed(GtkTreeView * treeview,GtkTreePath * arg1,GtkTreeViewColumn * arg2,TotemPlaylist * playlist)669 treeview_row_changed (GtkTreeView *treeview, GtkTreePath *arg1,
670 		GtkTreeViewColumn *arg2, TotemPlaylist *playlist)
671 {
672 	if (gtk_tree_path_compare (arg1, playlist->priv->current) == 0) {
673 		g_signal_emit (G_OBJECT (playlist),
674 				totem_playlist_table_signals[ITEM_ACTIVATED], 0,
675 				NULL);
676 		return;
677 	}
678 
679 	if (playlist->priv->current != NULL) {
680 		totem_playlist_unset_playing (playlist);
681 		gtk_tree_path_free (playlist->priv->current);
682 	}
683 
684 	playlist->priv->current = gtk_tree_path_copy (arg1);
685 
686 	g_signal_emit (G_OBJECT (playlist),
687 			totem_playlist_table_signals[CHANGED], 0,
688 			NULL);
689 }
690 
691 static gboolean
search_equal_is_match(const gchar * s,const gchar * lc_key)692 search_equal_is_match (const gchar * s, const gchar * lc_key)
693 {
694 	gboolean match = FALSE;
695 
696 	if (s != NULL) {
697 		gchar *lc_s;
698 
699 		/* maybe also normalize both strings? */
700 		lc_s = g_utf8_strdown (s, -1);
701 		match = (lc_s != NULL && strstr (lc_s, lc_key) != NULL);
702 		g_free (lc_s);
703 	}
704 
705 	return match;
706 }
707 
708 static gboolean
search_equal_func(GtkTreeModel * model,gint col,const gchar * key,GtkTreeIter * iter,gpointer userdata)709 search_equal_func (GtkTreeModel *model, gint col, const gchar *key,
710                    GtkTreeIter *iter, gpointer userdata)
711 {
712 	gboolean match;
713 	gchar *lc_key, *fn = NULL;
714 
715 	lc_key = g_utf8_strdown (key, -1);
716 
717         /* type-ahead search: first check display filename / title, then URI */
718 	gtk_tree_model_get (model, iter, FILENAME_COL, &fn, -1);
719 	match = search_equal_is_match (fn, lc_key);
720 	g_free (fn);
721 
722 	if (!match) {
723 		gchar *uri = NULL;
724 
725 		gtk_tree_model_get (model, iter, URI_COL, &uri, -1);
726 		fn = g_filename_from_uri (uri, NULL, NULL);
727 		match = search_equal_is_match (fn, lc_key);
728 		g_free (fn);
729 		g_free (uri);
730 	}
731 
732 	g_free (lc_key);
733 	return !match; /* needs to return FALSE if row matches */
734 }
735 
736 static void
init_treeview(GtkWidget * treeview,TotemPlaylist * playlist)737 init_treeview (GtkWidget *treeview, TotemPlaylist *playlist)
738 {
739 	GtkTreeSelection *selection;
740 
741 	init_columns (GTK_TREE_VIEW (treeview), playlist);
742 
743 	selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
744 	gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
745 	g_signal_connect (G_OBJECT (selection), "changed",
746 			G_CALLBACK (selection_changed), playlist);
747 	g_signal_connect (G_OBJECT (treeview), "row-activated",
748 			G_CALLBACK (treeview_row_changed), playlist);
749 
750 	playlist->priv->selection = selection;
751 
752 	/* make type-ahead search work in the playlist */
753 	gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (treeview),
754 	                                     search_equal_func, NULL, NULL);
755 
756 	gtk_widget_show (treeview);
757 }
758 
759 static void
update_repeat_cb(GSettings * settings,const gchar * key,TotemPlaylist * playlist)760 update_repeat_cb (GSettings *settings, const gchar *key, TotemPlaylist *playlist)
761 {
762 	playlist->priv->repeat = g_settings_get_boolean (settings, "repeat");
763 
764 	g_signal_emit (G_OBJECT (playlist),
765 			totem_playlist_table_signals[CHANGED], 0,
766 			NULL);
767 	g_object_notify (G_OBJECT (playlist), "repeat");
768 }
769 
770 static void
update_lockdown_cb(GSettings * settings,const gchar * key,TotemPlaylist * playlist)771 update_lockdown_cb (GSettings *settings, const gchar *key, TotemPlaylist *playlist)
772 {
773 	playlist->priv->disable_save_to_disk = g_settings_get_boolean (settings, "disable-save-to-disk");
774 }
775 
776 static void
init_config(TotemPlaylist * playlist)777 init_config (TotemPlaylist *playlist)
778 {
779 	playlist->priv->settings = g_settings_new (TOTEM_GSETTINGS_SCHEMA);
780 	playlist->priv->lockdown_settings = g_settings_new ("org.gnome.desktop.lockdown");
781 
782 	playlist->priv->disable_save_to_disk = g_settings_get_boolean (playlist->priv->lockdown_settings, "disable-save-to-disk");
783 
784 	g_signal_connect (playlist->priv->lockdown_settings, "changed::disable-save-to-disk",
785 			  G_CALLBACK (update_lockdown_cb), playlist);
786 
787 	playlist->priv->repeat = g_settings_get_boolean (playlist->priv->settings, "repeat");
788 
789 	g_signal_connect (playlist->priv->settings, "changed::repeat", (GCallback) update_repeat_cb, playlist);
790 }
791 
792 static gboolean
parse_bool_str(const char * str)793 parse_bool_str (const char *str)
794 {
795 	if (str == NULL)
796 		return FALSE;
797 	if (g_ascii_strcasecmp (str, "true") == 0)
798 		return TRUE;
799 	if (g_ascii_strcasecmp (str, "false") == 0)
800 		return FALSE;
801 	return atoi (str);
802 }
803 
804 static void
totem_playlist_entry_parsed(TotemPlParser * parser,const char * uri,GHashTable * metadata,TotemPlaylist * playlist)805 totem_playlist_entry_parsed (TotemPlParser *parser,
806 			     const char *uri,
807 			     GHashTable *metadata,
808 			     TotemPlaylist *playlist)
809 {
810 	const char *title, *content_type, *subtitle_uri, *starttime_str;
811 	gint64 duration, starttime;
812 	gboolean playing;
813 
814 	/* We ignore 0-length items in playlists, they're usually just banners */
815 	duration = totem_pl_parser_parse_duration
816 		(g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_DURATION), FALSE);
817 	if (duration == 0)
818 		return;
819 	title = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_TITLE);
820 	content_type = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_CONTENT_TYPE);
821 	playing = parse_bool_str (g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_PLAYING));
822 	subtitle_uri = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_SUBTITLE_URI);
823 	starttime_str = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_STARTTIME);
824 	starttime = totem_pl_parser_parse_duration (starttime_str, FALSE);
825 	starttime = MAX (starttime, 0);
826 	totem_playlist_add_one_mrl (playlist, uri, title, content_type, subtitle_uri, starttime, playing);
827 }
828 
829 static gboolean
totem_playlist_compare_with_monitor(TotemPlaylist * playlist,GtkTreeIter * iter,gconstpointer data)830 totem_playlist_compare_with_monitor (TotemPlaylist *playlist, GtkTreeIter *iter, gconstpointer data)
831 {
832 	GFileMonitor *monitor = (GFileMonitor *) data;
833 	GFileMonitor *_monitor;
834 	gboolean retval = FALSE;
835 
836 	gtk_tree_model_get (playlist->priv->model, iter,
837 			    FILE_MONITOR_COL, &_monitor, -1);
838 
839 	if (_monitor == monitor)
840 		retval = TRUE;
841 
842 	if (_monitor != NULL)
843 		g_object_unref (_monitor);
844 
845 	return retval;
846 }
847 
848 static void
totem_playlist_file_changed(GFileMonitor * monitor,GFile * file,GFile * other_file,GFileMonitorEvent event_type,TotemPlaylist * playlist)849 totem_playlist_file_changed (GFileMonitor *monitor,
850 			     GFile *file,
851 			     GFile *other_file,
852 			     GFileMonitorEvent event_type,
853 			     TotemPlaylist *playlist)
854 {
855 	if (event_type == G_FILE_MONITOR_EVENT_PRE_UNMOUNT ||
856 	    event_type == G_FILE_MONITOR_EVENT_UNMOUNTED) {
857 		totem_playlist_clear_with_compare (playlist,
858 						   (ClearComparisonFunc) totem_playlist_compare_with_monitor,
859 						   monitor);
860 	}
861 }
862 
863 static void
totem_playlist_dispose(GObject * object)864 totem_playlist_dispose (GObject *object)
865 {
866 	TotemPlaylist *playlist = TOTEM_PLAYLIST (object);
867 
868 	g_clear_object (&playlist->priv->parser);
869 	g_clear_object (&playlist->priv->settings);
870 	g_clear_object (&playlist->priv->lockdown_settings);
871 	g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
872 
873 	G_OBJECT_CLASS (totem_playlist_parent_class)->dispose (object);
874 }
875 
876 static void
totem_playlist_init(TotemPlaylist * playlist)877 totem_playlist_init (TotemPlaylist *playlist)
878 {
879 	GtkWidget *container;
880 	GtkBuilder *xml;
881 	GtkWidget *widget;
882 	GtkStyleContext *context;
883 
884 	gtk_orientable_set_orientation (GTK_ORIENTABLE (playlist),
885 					GTK_ORIENTATION_VERTICAL);
886 
887 	playlist->priv = totem_playlist_get_instance_private (playlist);
888 	playlist->priv->parser = totem_pl_parser_new ();
889 
890 	totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "dvd:");
891 	totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "vcd:");
892 	totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "cd:");
893 	totem_pl_parser_add_ignored_scheme (playlist->priv->parser, "dvb:");
894 	totem_pl_parser_add_ignored_mimetype (playlist->priv->parser, "application/x-trash");
895 	totem_pl_parser_add_ignored_mimetype (playlist->priv->parser, "text/html");
896 	totem_pl_parser_add_ignored_mimetype (playlist->priv->parser, "application/x-ms-dos-executable");
897 	totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.htm");
898 	totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.html");
899 	totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.nfo");
900 	totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.txt");
901 	totem_pl_parser_add_ignored_glob (playlist->priv->parser, "*.exe");
902 
903 	g_signal_connect (G_OBJECT (playlist->priv->parser),
904 			"entry-parsed",
905 			G_CALLBACK (totem_playlist_entry_parsed),
906 			playlist);
907 
908 	xml = totem_interface_load ("playlist.ui", TRUE, NULL, playlist);
909 
910 	if (xml == NULL)
911 		return;
912 
913 	gtk_widget_add_events (GTK_WIDGET (playlist), GDK_KEY_PRESS_MASK);
914 	g_signal_connect (G_OBJECT (playlist), "key_press_event",
915 			  G_CALLBACK (totem_playlist_key_press), playlist);
916 
917 	/* Buttons */
918 	playlist->priv->remove_button = GTK_WIDGET (gtk_builder_get_object (xml, "remove_button"));
919 
920 	/* Join treeview and buttons */
921 	widget = GTK_WIDGET (gtk_builder_get_object (xml, ("scrolledwindow1")));
922 	context = gtk_widget_get_style_context (widget);
923 	gtk_style_context_set_junction_sides (context, GTK_JUNCTION_BOTTOM);
924 	widget = GTK_WIDGET (gtk_builder_get_object (xml, ("toolbar1")));
925 	context = gtk_widget_get_style_context (widget);
926 	gtk_style_context_set_junction_sides (context, GTK_JUNCTION_TOP);
927 
928 	/* Reparent the vbox */
929 	container = GTK_WIDGET (gtk_builder_get_object (xml, "vbox4"));
930 	g_object_ref (container);
931 	gtk_box_pack_start (GTK_BOX (playlist),
932 			container,
933 			TRUE,       /* expand */
934 			TRUE,       /* fill */
935 			0);         /* padding */
936 	g_object_unref (container);
937 
938 	playlist->priv->treeview = GTK_WIDGET (gtk_builder_get_object (xml, "treeview1"));
939 	init_treeview (playlist->priv->treeview, playlist);
940 	playlist->priv->model = gtk_tree_view_get_model
941 		(GTK_TREE_VIEW (playlist->priv->treeview));
942 
943 	/* tooltips */
944 	gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(playlist->priv->treeview),
945 					 FILENAME_ESCAPED_COL);
946 
947 	/* The configuration */
948 	init_config (playlist);
949 
950 	gtk_widget_show_all (GTK_WIDGET (playlist));
951 
952 	g_object_unref (xml);
953 }
954 
955 GtkWidget*
totem_playlist_new(void)956 totem_playlist_new (void)
957 {
958 	return GTK_WIDGET (g_object_new (TOTEM_TYPE_PLAYLIST, NULL));
959 }
960 
961 static gboolean
totem_playlist_add_one_mrl(TotemPlaylist * playlist,const char * mrl,const char * display_name,const char * content_type,const char * subtitle_uri,gint64 starttime,gboolean playing)962 totem_playlist_add_one_mrl (TotemPlaylist *playlist,
963 			    const char *mrl,
964 			    const char *display_name,
965 			    const char *content_type,
966 			    const char *subtitle_uri,
967 			    gint64      starttime,
968 			    gboolean    playing)
969 {
970 	GtkListStore *store;
971 	GtkTreeIter iter;
972 	char *filename_for_display, *uri, *escaped_filename;
973 	GFileMonitor *monitor;
974 	GMount *mount;
975 	GFile *file;
976 
977 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
978 	g_return_val_if_fail (mrl != NULL, FALSE);
979 
980 	if (display_name == NULL || *display_name == '\0')
981 		filename_for_display = totem_playlist_mrl_to_title (mrl);
982 	else
983 		filename_for_display = g_strdup (display_name);
984 
985 	uri = totem_create_full_path (mrl);
986 
987 	g_debug ("totem_playlist_add_one_mrl (): %s %s %s %s %"G_GINT64_FORMAT " %s\n", filename_for_display, uri, display_name, subtitle_uri, starttime, playing ? "true" : "false");
988 
989 	store = GTK_LIST_STORE (playlist->priv->model);
990 
991 	/* Get the file monitor */
992 	file = g_file_new_for_uri (uri ? uri : mrl);
993 	if (g_file_is_native (file) != FALSE) {
994 		monitor = g_file_monitor_file (file,
995 					       G_FILE_MONITOR_NONE,
996 					       NULL,
997 					       NULL);
998 		g_signal_connect (G_OBJECT (monitor),
999 				  "changed",
1000 				  G_CALLBACK (totem_playlist_file_changed),
1001 				  playlist);
1002 		mount = NULL;
1003 	} else {
1004 		mount = totem_get_mount_for_media (uri ? uri : mrl);
1005 		monitor = NULL;
1006 	}
1007 
1008 	escaped_filename = g_markup_escape_text (filename_for_display, -1);
1009 	gtk_list_store_insert_with_values (store, &iter, -1,
1010 					   PLAYING_COL, playing ? TOTEM_PLAYLIST_STATUS_PAUSED : TOTEM_PLAYLIST_STATUS_NONE,
1011 					   FILENAME_COL, filename_for_display,
1012 					   FILENAME_ESCAPED_COL, escaped_filename,
1013 					   URI_COL, uri ? uri : mrl,
1014 					   SUBTITLE_URI_COL, subtitle_uri,
1015 					   TITLE_CUSTOM_COL, display_name ? TRUE : FALSE,
1016 					   FILE_MONITOR_COL, monitor,
1017 					   MOUNT_COL, mount,
1018 					   MIME_TYPE_COL, content_type,
1019 					   STARTTIME_COL, starttime,
1020 					   -1);
1021 	g_free (escaped_filename);
1022 
1023 	g_signal_emit (playlist,
1024 		       totem_playlist_table_signals[ITEM_ADDED],
1025 		       0, filename_for_display, uri ? uri : mrl);
1026 
1027 	g_free (filename_for_display);
1028 	g_free (uri);
1029 
1030 	if (playlist->priv->current == NULL)
1031 		playlist->priv->current = gtk_tree_model_get_path (playlist->priv->model, &iter);
1032 
1033 	g_signal_emit (G_OBJECT (playlist),
1034 			totem_playlist_table_signals[CHANGED], 0,
1035 			NULL);
1036 
1037 	return TRUE;
1038 }
1039 
1040 typedef struct {
1041 	GAsyncReadyCallback callback;
1042 	gpointer user_data;
1043 	gboolean cursor;
1044 	TotemPlaylist *playlist;
1045 	gchar *mrl;
1046 	gchar *display_name;
1047 } AddMrlData;
1048 
1049 static void
add_mrl_data_free(AddMrlData * data)1050 add_mrl_data_free (AddMrlData *data)
1051 {
1052 	g_object_unref (data->playlist);
1053 	g_free (data->mrl);
1054 	g_free (data->display_name);
1055 	g_slice_free (AddMrlData, data);
1056 }
1057 
1058 static gboolean
handle_parse_result(TotemPlParserResult res,TotemPlaylist * playlist,const gchar * mrl,const gchar * display_name,GError ** error)1059 handle_parse_result (TotemPlParserResult res, TotemPlaylist *playlist, const gchar *mrl, const gchar *display_name, GError **error)
1060 {
1061 	if (res == TOTEM_PL_PARSER_RESULT_UNHANDLED)
1062 		return totem_playlist_add_one_mrl (playlist, mrl, display_name, NULL, NULL, 0, FALSE);
1063 	if (res == TOTEM_PL_PARSER_RESULT_ERROR) {
1064 		g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
1065 			     _("The playlist “%s” could not be parsed. It might be damaged."), display_name ? display_name : mrl);
1066 
1067 		return FALSE;
1068 	}
1069 	if (res == TOTEM_PL_PARSER_RESULT_IGNORED)
1070 		return FALSE;
1071 
1072 	return TRUE;
1073 }
1074 
1075 static void
add_mrl_cb(TotemPlParser * parser,GAsyncResult * result,AddMrlData * data)1076 add_mrl_cb (TotemPlParser *parser, GAsyncResult *result, AddMrlData *data)
1077 {
1078 	TotemPlParserResult res;
1079 	GSimpleAsyncResult *async_result;
1080 	GError *error = NULL;
1081 	gboolean ret;
1082 
1083 	g_assert (data != NULL);
1084 
1085 	/* Finish parsing the playlist */
1086 	res = totem_pl_parser_parse_finish (parser, result, NULL);
1087 
1088 	/* Remove the cursor, if one was set */
1089 	if (data->cursor)
1090 		g_application_unmark_busy (g_application_get_default ());
1091 
1092 	/* Create an async result which will return the result to the code which called totem_playlist_add_mrl() */
1093 	ret = handle_parse_result (res, data->playlist, data->mrl, data->display_name, &error);
1094 	async_result = g_simple_async_result_new (G_OBJECT (data->playlist), data->callback, data->user_data, totem_playlist_add_mrl);
1095 	if (error != NULL)
1096 		g_simple_async_result_take_error (async_result, error);
1097 
1098 	/* Handle the various return cases from the playlist parser */
1099 	g_simple_async_result_set_op_res_gboolean (async_result, ret);
1100 
1101 	/* Free the closure's data, now that we're finished with it */
1102 	add_mrl_data_free (data);
1103 
1104 	/* Synchronously call the calling code's callback function (i.e. what was passed to totem_playlist_add_mrl()'s @callback parameter)
1105 	 * in the main thread to return the result */
1106 	g_simple_async_result_complete (async_result);
1107 }
1108 
1109 void
totem_playlist_add_mrl(TotemPlaylist * playlist,const char * mrl,const char * display_name,gboolean cursor,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1110 totem_playlist_add_mrl (TotemPlaylist *playlist, const char *mrl, const char *display_name, gboolean cursor,
1111                         GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
1112 {
1113 	AddMrlData *data;
1114 
1115 	g_return_if_fail (mrl != NULL);
1116 
1117 	/* Display a waiting cursor if required */
1118 	if (cursor)
1119 		g_application_mark_busy (g_application_get_default ());
1120 
1121 	/* Build the data struct to pass to the callback function */
1122 	data = g_slice_new (AddMrlData);
1123 	data->callback = callback;
1124 	data->user_data = user_data;
1125 	data->cursor = cursor;
1126 	data->playlist = g_object_ref (playlist);
1127 	data->mrl = g_strdup (mrl);
1128 	data->display_name = g_strdup (display_name);
1129 
1130 	/* Start parsing the playlist. Once this is complete, add_mrl_cb() is called, which will interpret the results and call @callback to
1131 	 * finish the process. */
1132 	totem_pl_parser_parse_async (playlist->priv->parser, mrl, FALSE, cancellable, (GAsyncReadyCallback) add_mrl_cb, data);
1133 }
1134 
1135 gboolean
totem_playlist_add_mrl_finish(TotemPlaylist * playlist,GAsyncResult * result,GError ** error)1136 totem_playlist_add_mrl_finish (TotemPlaylist *playlist, GAsyncResult *result, GError **error)
1137 {
1138 	g_assert (g_simple_async_result_get_source_tag (G_SIMPLE_ASYNC_RESULT (result)) == totem_playlist_add_mrl);
1139 
1140 	if (g_simple_async_result_get_op_res_gboolean (G_SIMPLE_ASYNC_RESULT (result)))
1141 		return TRUE;
1142 	g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error);
1143 	return FALSE;
1144 }
1145 
1146 gboolean
totem_playlist_add_mrl_sync(TotemPlaylist * playlist,const char * mrl)1147 totem_playlist_add_mrl_sync (TotemPlaylist *playlist,
1148 			     const char    *mrl)
1149 {
1150 	GtkTreeIter iter;
1151 	gboolean ret;
1152 
1153 	g_return_val_if_fail (mrl != NULL, FALSE);
1154 
1155 	ret = handle_parse_result (totem_pl_parser_parse (playlist->priv->parser, mrl, FALSE), playlist, mrl, NULL, NULL);
1156 	if (!ret)
1157 		return ret;
1158 
1159 	/* Find the currently playing track, and set ->current */
1160 	ret = gtk_tree_model_get_iter_first (playlist->priv->model, &iter);
1161 	while (ret) {
1162 		TotemPlaylistStatus status;
1163 
1164 		gtk_tree_model_get (playlist->priv->model, &iter,
1165 				    PLAYING_COL, &status,
1166 				    -1);
1167 		if (status == TOTEM_PLAYLIST_STATUS_PAUSED) {
1168 			gtk_tree_path_free (playlist->priv->current);
1169 			playlist->priv->current = gtk_tree_model_get_path (playlist->priv->model, &iter);
1170 			break;
1171 		}
1172 		ret = gtk_tree_model_iter_next (playlist->priv->model, &iter);
1173 	}
1174 
1175 	return TRUE;
1176 }
1177 
1178 typedef struct {
1179 	TotemPlaylist *playlist;
1180 	GList *mrls; /* list of TotemPlaylistMrlDatas */
1181 	gboolean cursor;
1182 	GAsyncReadyCallback callback;
1183 	gpointer user_data;
1184 
1185 	guint next_index_to_add;
1186 	GList *unadded_entries; /* list of TotemPlaylistMrlDatas */
1187 	volatile gint entries_remaining;
1188 } AddMrlsOperationData;
1189 
1190 static void
add_mrls_operation_data_free(AddMrlsOperationData * data)1191 add_mrls_operation_data_free (AddMrlsOperationData *data)
1192 {
1193 	/* Remove the cursor, if one was set */
1194 	if (data->cursor)
1195 		g_application_unmark_busy (g_application_get_default ());
1196 
1197 	g_list_free_full (data->mrls, (GDestroyNotify) totem_playlist_mrl_data_free);
1198 	g_object_unref (data->playlist);
1199 
1200 	g_slice_free (AddMrlsOperationData, data);
1201 }
1202 
1203 struct TotemPlaylistMrlData {
1204 	gchar *mrl;
1205 	gchar *display_name;
1206 	TotemPlParserResult res;
1207 
1208 	/* Implementation details */
1209 	AddMrlsOperationData *operation_data;
1210 	guint index;
1211 };
1212 
1213 /**
1214  * totem_playlist_mrl_data_new:
1215  * @mrl: a MRL
1216  * @display_name: (allow-none): a human-readable display name for the MRL, or %NULL
1217  *
1218  * Create a new #TotemPlaylistMrlData struct storing the given @mrl and @display_name.
1219  *
1220  * This will typically be immediately appended to a #GList to be passed to totem_playlist_add_mrls().
1221  *
1222  * Return value: (transfer full): a new #TotemPlaylistMrlData; free with totem_playlist_mrl_data_free()
1223  *
1224  * Since: 3.0
1225  */
1226 TotemPlaylistMrlData *
totem_playlist_mrl_data_new(const gchar * mrl,const gchar * display_name)1227 totem_playlist_mrl_data_new (const gchar *mrl,
1228                              const gchar *display_name)
1229 {
1230 	TotemPlaylistMrlData *data;
1231 
1232 	g_return_val_if_fail (mrl != NULL && *mrl != '\0', NULL);
1233 
1234 	data = g_slice_new (TotemPlaylistMrlData);
1235 	data->mrl = g_strdup (mrl);
1236 	data->display_name = g_strdup (display_name);
1237 
1238 	return data;
1239 }
1240 
1241 /**
1242  * totem_playlist_mrl_data_free:
1243  * @data: (transfer full): a #TotemPlaylistMrlData
1244  *
1245  * Free the given #TotemPlaylistMrlData struct. This should not generally be called by code outside #TotemPlaylist.
1246  *
1247  * Since: 3.0
1248  */
1249 void
totem_playlist_mrl_data_free(TotemPlaylistMrlData * data)1250 totem_playlist_mrl_data_free (TotemPlaylistMrlData *data)
1251 {
1252 	g_return_if_fail (data != NULL);
1253 
1254 	/* NOTE: This doesn't call add_mrls_operation_data_free() on @data->operation_data, since it's shared with other instances of
1255 	 * TotemPlaylistMrlData, and not truly reference counted. */
1256 	g_free (data->display_name);
1257 	g_free (data->mrl);
1258 
1259 	g_slice_free (TotemPlaylistMrlData, data);
1260 }
1261 
1262 static void
add_mrls_finish_operation(AddMrlsOperationData * operation_data)1263 add_mrls_finish_operation (AddMrlsOperationData *operation_data)
1264 {
1265 	/* Check whether this is the final callback invocation; iff it is, we can call the user's callback for the entire operation and free the
1266 	 * operation data */
1267 	if (g_atomic_int_dec_and_test (&(operation_data->entries_remaining)) == TRUE) {
1268 		GSimpleAsyncResult *async_result;
1269 
1270 		async_result = g_simple_async_result_new (G_OBJECT (operation_data->playlist), operation_data->callback, operation_data->user_data,
1271 		                                          totem_playlist_add_mrls);
1272 		g_simple_async_result_complete (async_result);
1273 		g_object_unref (async_result);
1274 
1275 		add_mrls_operation_data_free (operation_data);
1276 	}
1277 }
1278 
1279 /* Called exactly once for each MRL in a totem_playlist_add_mrls() operation. Called in the thread running the main loop. If the MRL which has just
1280  * been parsed is the next one in the sequence (of entries in @mrls as passed to totem_playlist_add_mrls()), it's added to the playlist proper.
1281  * Otherwise, it's added to a sorted queue of MRLs which have had their callbacks called out of order.
1282  * When a MRL is added to the playlist proper, any successor MRLs which are in the sorted queue are also added to the playlist proper.
1283  * When add_mrls_cb() is called for the last time for a given call to totem_playlist_add_mrls(), it calls the user's callback for the operation
1284  * (passed as @callback to totem_playlist_add_mrls()) and frees the #AddMrlsOperationData struct. This is handled by add_mrls_finish_operation().
1285  * The #TotemPlaylistMrlData for each MRL is freed by add_mrls_operation_data_free() at the end of the entire operation. */
1286 static void
add_mrls_cb(TotemPlParser * parser,GAsyncResult * result,TotemPlaylistMrlData * mrl_data)1287 add_mrls_cb (TotemPlParser *parser, GAsyncResult *result, TotemPlaylistMrlData *mrl_data)
1288 {
1289 	AddMrlsOperationData *operation_data = mrl_data->operation_data;
1290 
1291 	/* Finish parsing the playlist */
1292 	mrl_data->res = totem_pl_parser_parse_finish (parser, result, NULL);
1293 
1294 	g_assert (mrl_data->index >= operation_data->next_index_to_add);
1295 
1296 	if (mrl_data->index == operation_data->next_index_to_add) {
1297 		GList *i;
1298 
1299 		/* The entry is the next one in the order, so doesn't need to be added to the unadded list, and can be added to playlist proper */
1300 		operation_data->next_index_to_add++;
1301 		handle_parse_result (mrl_data->res, operation_data->playlist, mrl_data->mrl, mrl_data->display_name, NULL);
1302 
1303 		/* See if we can now add any other entries which have already been processed */
1304 		for (i = operation_data->unadded_entries;
1305 		     i != NULL && ((TotemPlaylistMrlData*) i->data)->index == operation_data->next_index_to_add;
1306 		     i = g_list_delete_link (i, i)) {
1307 			TotemPlaylistMrlData *_mrl_data = (TotemPlaylistMrlData*) i->data;
1308 
1309 			operation_data->next_index_to_add++;
1310 			handle_parse_result (_mrl_data->res, operation_data->playlist, _mrl_data->mrl, _mrl_data->display_name, NULL);
1311 		}
1312 
1313 		operation_data->unadded_entries = i;
1314 	} else {
1315 		GList *i;
1316 
1317 		/* The entry has been parsed out of order, so needs to be added (in the correct position) to the unadded list for latter addition to
1318 		 * the playlist proper */
1319 		for (i = operation_data->unadded_entries; i != NULL && mrl_data->index > ((TotemPlaylistMrlData*) i->data)->index; i = i->next);
1320 		operation_data->unadded_entries = g_list_insert_before (operation_data->unadded_entries, i, mrl_data);
1321 	}
1322 
1323 	/* Check whether this is the last callback; call the user's callback for the entire operation and free the operation data if appropriate */
1324 	add_mrls_finish_operation (operation_data);
1325 }
1326 
1327 /**
1328  * totem_playlist_add_mrls:
1329  * @self: a #TotemPlaylist
1330  * @mrls: (element-type TotemPlaylistMrlData) (transfer full): a list of #TotemPlaylistMrlData structs
1331  * @cursor: %TRUE to set a waiting cursor on the playlist for the duration of the operation, %FALSE otherwise
1332  * @cancellable: (allow-none): a #Cancellable, or %NULL
1333  * @callback: (scope async) (allow-none): callback to call once all the MRLs have been added to the playlist, or %NULL
1334  * @user_data: (closure) (allow-none): user data to pass to @callback, or %NULL
1335  *
1336  * Add the MRLs listed in @mrls to the playlist asynchronously, and ensuring that they're added to the playlist in the order they appear in the
1337  * input #GList.
1338  *
1339  * @mrls should be a #GList of #TotemPlaylistMrlData structs, each created with totem_playlist_mrl_data_new(). This function takes ownership of both
1340  * the list and its elements when called, so don't free either after calling totem_playlist_add_mrls().
1341  *
1342  * @callback will be called after all the MRLs in @mrls have been parsed and (if they were parsed successfully) added to the playlist. In the
1343  * callback function, totem_playlist_add_mrls_finish() should be called to check for errors.
1344  *
1345  * Since: 3.0
1346  */
1347 void
totem_playlist_add_mrls(TotemPlaylist * self,GList * mrls,gboolean cursor,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1348 totem_playlist_add_mrls (TotemPlaylist *self,
1349                          GList *mrls,
1350                          gboolean cursor,
1351                          GCancellable *cancellable,
1352                          GAsyncReadyCallback callback,
1353                          gpointer user_data)
1354 {
1355 	AddMrlsOperationData *operation_data;
1356 	GList *i;
1357 	guint mrl_index = 0;
1358 
1359 	g_return_if_fail (TOTEM_IS_PLAYLIST (self));
1360 	g_return_if_fail (mrls != NULL);
1361 	g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
1362 
1363 	/* Build the data struct to pass to the callback function */
1364 	operation_data = g_slice_new (AddMrlsOperationData);
1365 	operation_data->playlist = g_object_ref (self);
1366 	operation_data->mrls = mrls;
1367 	operation_data->cursor = cursor;
1368 	operation_data->callback = callback;
1369 	operation_data->user_data = user_data;
1370 	operation_data->next_index_to_add = mrl_index;
1371 	operation_data->unadded_entries = NULL;
1372 	g_atomic_int_set (&(operation_data->entries_remaining), 1);
1373 
1374 	/* Display a waiting cursor if required */
1375 	if (cursor)
1376 		g_application_mark_busy (g_application_get_default ());
1377 
1378 	for (i = mrls; i != NULL; i = i->next) {
1379 		TotemPlaylistMrlData *mrl_data = (TotemPlaylistMrlData*) i->data;
1380 
1381 		if (mrl_data == NULL)
1382 			continue;
1383 
1384 		/* Set the item's parsing index, so that it's inserted into the playlist in the position it appeared in @mrls */
1385 		mrl_data->operation_data = operation_data;
1386 		mrl_data->index = mrl_index++;
1387 
1388 		g_atomic_int_inc (&(operation_data->entries_remaining));
1389 
1390 		/* Start parsing the playlist. Once this is complete, add_mrls_cb() is called (i.e. it's called exactly once for each entry in
1391 		 * @mrls).
1392 		 * TODO: Cancellation is currently not supoprted, since no consumers of this API make use of it, and it needs careful thought when
1393 		 * being implemented, as a separate #GCancellable instance will have to be created for each parallel computation. */
1394 		totem_pl_parser_parse_async (self->priv->parser, mrl_data->mrl, FALSE, NULL, (GAsyncReadyCallback) add_mrls_cb, mrl_data);
1395 	}
1396 
1397 	/* Deal with the case that all async operations completed before we got to this point (since we've held a reference to the operation data so
1398 	 * that it doesn't get freed prematurely if all the scheduled async parse operations complete before we've finished scheduling the rest. */
1399 	add_mrls_finish_operation (operation_data);
1400 }
1401 
1402 /**
1403  * totem_playlist_add_mrls_finish:
1404  * @self: a #TotemPlaylist
1405  * @result: the #GAsyncResult that was provided to the callback
1406  * @error: (allow-none): a #GError for error reporting, or %NULL
1407  *
1408  * Finish an asynchronous batch MRL addition operation started by totem_playlist_add_mrls().
1409  *
1410  * Return value: %TRUE on success, %FALSE otherwise
1411  *
1412  * Since: 3.0
1413  */
1414 gboolean
totem_playlist_add_mrls_finish(TotemPlaylist * self,GAsyncResult * result,GError ** error)1415 totem_playlist_add_mrls_finish (TotemPlaylist *self,
1416                                 GAsyncResult *result,
1417                                 GError **error)
1418 {
1419 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (self), FALSE);
1420 	g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
1421 	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
1422 	g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self), totem_playlist_add_mrls), FALSE);
1423 
1424 	/* We don't have anything to return at the moment. */
1425 	return TRUE;
1426 }
1427 
1428 static gboolean
totem_playlist_clear_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)1429 totem_playlist_clear_cb (GtkTreeModel *model,
1430 			 GtkTreePath  *path,
1431 			 GtkTreeIter  *iter,
1432 			 gpointer      data)
1433 {
1434 	totem_playlist_emit_item_removed (data, iter);
1435 	return FALSE;
1436 }
1437 
1438 gboolean
totem_playlist_clear(TotemPlaylist * playlist)1439 totem_playlist_clear (TotemPlaylist *playlist)
1440 {
1441 	GtkListStore *store;
1442 
1443 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1444 
1445 	if (PL_LEN == 0)
1446 		return FALSE;
1447 
1448 	gtk_tree_model_foreach (playlist->priv->model,
1449 				totem_playlist_clear_cb,
1450 				playlist);
1451 
1452 	store = GTK_LIST_STORE (playlist->priv->model);
1453 	gtk_list_store_clear (store);
1454 
1455 	g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
1456 
1457 	g_signal_emit (G_OBJECT (playlist),
1458 		       totem_playlist_table_signals[CURRENT_REMOVED],
1459 		       0, NULL);
1460 
1461 	return TRUE;
1462 }
1463 
1464 static int
compare_removal(GtkTreeRowReference * ref,GtkTreePath * path)1465 compare_removal (GtkTreeRowReference *ref, GtkTreePath *path)
1466 {
1467 	GtkTreePath *ref_path;
1468 	int ret = -1;
1469 
1470 	ref_path = gtk_tree_row_reference_get_path (ref);
1471 	if (gtk_tree_path_compare (path, ref_path) == 0)
1472 		ret = 0;
1473 	gtk_tree_path_free (ref_path);
1474 	return ret;
1475 }
1476 
1477 /* Whether the item in question will be removed */
1478 static gboolean
totem_playlist_item_to_be_removed(TotemPlaylist * playlist,GtkTreePath * path,ClearComparisonFunc func)1479 totem_playlist_item_to_be_removed (TotemPlaylist *playlist,
1480 				   GtkTreePath *path,
1481 				   ClearComparisonFunc func)
1482 {
1483 	GList *ret;
1484 
1485 	if (func == NULL) {
1486 		GtkTreeSelection *selection;
1487 
1488 		selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (playlist->priv->treeview));
1489 		return gtk_tree_selection_path_is_selected (selection, path);
1490 	}
1491 
1492 	ret = g_list_find_custom (playlist->priv->list, path, (GCompareFunc) compare_removal);
1493 	return (ret != NULL);
1494 }
1495 
1496 static void
totem_playlist_clear_with_compare(TotemPlaylist * playlist,ClearComparisonFunc func,gconstpointer data)1497 totem_playlist_clear_with_compare (TotemPlaylist *playlist,
1498 				   ClearComparisonFunc func,
1499 				   gconstpointer data)
1500 {
1501 	GtkTreeRowReference *ref;
1502 	GtkTreeRowReference *next;
1503 
1504 	ref = NULL;
1505 	next = NULL;
1506 
1507 	if (func == NULL) {
1508 		GtkTreeSelection *selection;
1509 
1510 		selection = gtk_tree_view_get_selection
1511 			(GTK_TREE_VIEW (playlist->priv->treeview));
1512 		if (selection == NULL)
1513 			return;
1514 
1515 		gtk_tree_selection_selected_foreach (selection,
1516 						     totem_playlist_foreach_selected,
1517 						     (gpointer) playlist);
1518 	} else {
1519 		guint num_items, i;
1520 
1521 		num_items = PL_LEN;
1522 		if (num_items == 0)
1523 			return;
1524 
1525 		for (i = 0; i < num_items; i++) {
1526 			GtkTreeIter iter;
1527 			char *playlist_index;
1528 
1529 			playlist_index = g_strdup_printf ("%d", i);
1530 			if (gtk_tree_model_get_iter_from_string (playlist->priv->model, &iter, playlist_index) == FALSE) {
1531 				g_free (playlist_index);
1532 				continue;
1533 			}
1534 			g_free (playlist_index);
1535 
1536 			if ((* func) (playlist, &iter, data) != FALSE) {
1537 				GtkTreePath *path;
1538 				GtkTreeRowReference *r;
1539 
1540 				path = gtk_tree_path_new_from_indices (i, -1);
1541 				r = gtk_tree_row_reference_new (playlist->priv->model, path);
1542 				if (playlist->priv->current_to_be_removed == FALSE && playlist->priv->current != NULL) {
1543 					if (gtk_tree_path_compare (path, playlist->priv->current) == 0) {
1544 						playlist->priv->current_to_be_removed = TRUE;
1545 					}
1546 				}
1547 				playlist->priv->list = g_list_prepend (playlist->priv->list, r);
1548 				gtk_tree_path_free (path);
1549 			}
1550 		}
1551 
1552 		if (playlist->priv->list == NULL)
1553 			return;
1554 	}
1555 
1556 	/* If the current item is to change, we need to keep an static
1557 	 * reference to it, TreeIter and TreePath don't allow that */
1558 	if (playlist->priv->current_to_be_removed == FALSE &&
1559 	    playlist->priv->current != NULL) {
1560 		ref = gtk_tree_row_reference_new (playlist->priv->model,
1561 						  playlist->priv->current);
1562 	} else if (playlist->priv->current != NULL) {
1563 		GtkTreePath *item;
1564 
1565 		item = gtk_tree_path_copy (playlist->priv->current);
1566 		gtk_tree_path_next (item);
1567 		next = gtk_tree_row_reference_new (playlist->priv->model, item);
1568 		while (next != NULL) {
1569 			if (totem_playlist_item_to_be_removed (playlist, item, func) == FALSE) {
1570 				/* Found the item after the current one that
1571 				 * won't be removed, thus the new current */
1572 				break;
1573 			}
1574 			gtk_tree_row_reference_free (next);
1575 			gtk_tree_path_next (item);
1576 			next = gtk_tree_row_reference_new (playlist->priv->model, item);
1577 		}
1578 	}
1579 
1580 	/* We destroy the items, one-by-one from the list built above */
1581 	while (playlist->priv->list != NULL) {
1582 		GtkTreePath *path;
1583 		GtkTreeIter iter;
1584 
1585 		path = gtk_tree_row_reference_get_path
1586 			((GtkTreeRowReference *)(playlist->priv->list->data));
1587 		gtk_tree_model_get_iter (playlist->priv->model, &iter, path);
1588 		gtk_tree_path_free (path);
1589 
1590 		totem_playlist_emit_item_removed (playlist, &iter);
1591 		gtk_list_store_remove (GTK_LIST_STORE (playlist->priv->model), &iter);
1592 
1593 		gtk_tree_row_reference_free
1594 			((GtkTreeRowReference *)(playlist->priv->list->data));
1595 		playlist->priv->list = g_list_remove (playlist->priv->list,
1596 				playlist->priv->list->data);
1597 	}
1598 	g_clear_pointer (&playlist->priv->list, g_list_free);
1599 
1600 	if (playlist->priv->current_to_be_removed != FALSE) {
1601 		/* The current item was removed from the playlist */
1602 		if (next != NULL) {
1603 			playlist->priv->current = gtk_tree_row_reference_get_path (next);
1604 			gtk_tree_row_reference_free (next);
1605 		} else {
1606 			playlist->priv->current = NULL;
1607 		}
1608 
1609 		g_signal_emit (G_OBJECT (playlist),
1610 				totem_playlist_table_signals[CURRENT_REMOVED],
1611 				0, NULL);
1612 	} else {
1613 		if (ref != NULL) {
1614 			/* The path to the current item changed */
1615 			playlist->priv->current = gtk_tree_row_reference_get_path (ref);
1616 		}
1617 
1618 		g_signal_emit (G_OBJECT (playlist),
1619 				totem_playlist_table_signals[CHANGED], 0,
1620 				NULL);
1621 	}
1622 	if (ref != NULL)
1623 		gtk_tree_row_reference_free (ref);
1624 	gtk_tree_view_columns_autosize (GTK_TREE_VIEW (playlist->priv->treeview));
1625 
1626 	playlist->priv->current_to_be_removed = FALSE;
1627 }
1628 
1629 static char *
get_mount_default_location(GMount * mount)1630 get_mount_default_location (GMount *mount)
1631 {
1632 	GFile *file;
1633 	char *path;
1634 
1635 	file = g_mount_get_root (mount);
1636 	if (file == NULL)
1637 		return NULL;
1638 	path = g_file_get_path (file);
1639 	g_object_unref (file);
1640 	return path;
1641 }
1642 
1643 static gboolean
totem_playlist_compare_with_mount(TotemPlaylist * playlist,GtkTreeIter * iter,gconstpointer data)1644 totem_playlist_compare_with_mount (TotemPlaylist *playlist, GtkTreeIter *iter, gconstpointer data)
1645 {
1646 	GMount *clear_mount = (GMount *) data;
1647 	GMount *mount;
1648 	char *mount_path, *clear_mount_path;
1649 	gboolean retval = FALSE;
1650 
1651 	gtk_tree_model_get (playlist->priv->model, iter,
1652 			    MOUNT_COL, &mount, -1);
1653 
1654 	if (mount == NULL)
1655 		return FALSE;
1656 
1657 	clear_mount_path = NULL;
1658 
1659 	mount_path = get_mount_default_location (mount);
1660 	if (mount_path == NULL)
1661 		goto bail;
1662 
1663 	clear_mount_path = get_mount_default_location (clear_mount);
1664 	if (clear_mount_path == NULL)
1665 		goto bail;
1666 
1667 	if (g_str_equal (mount_path, clear_mount_path))
1668 		retval = TRUE;
1669 
1670 bail:
1671 	g_free (mount_path);
1672 	g_free (clear_mount_path);
1673 
1674 	if (mount != NULL)
1675 		g_object_unref (mount);
1676 
1677 	return retval;
1678 }
1679 
1680 void
totem_playlist_clear_with_g_mount(TotemPlaylist * playlist,GMount * mount)1681 totem_playlist_clear_with_g_mount (TotemPlaylist *playlist,
1682 				   GMount *mount)
1683 {
1684 	g_return_if_fail (mount != NULL);
1685 
1686 	totem_playlist_clear_with_compare (playlist,
1687 					   (ClearComparisonFunc) totem_playlist_compare_with_mount,
1688 					   mount);
1689 }
1690 
1691 char *
totem_playlist_get_current_mrl(TotemPlaylist * playlist,char ** subtitle)1692 totem_playlist_get_current_mrl (TotemPlaylist *playlist, char **subtitle)
1693 {
1694 	GtkTreeIter iter;
1695 	char *path;
1696 
1697 	if (subtitle != NULL)
1698 		*subtitle = NULL;
1699 
1700 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1701 
1702 	if (update_current_from_playlist (playlist) == FALSE)
1703 		return NULL;
1704 
1705 	if (gtk_tree_model_get_iter (playlist->priv->model, &iter,
1706 				     playlist->priv->current) == FALSE)
1707 		return NULL;
1708 
1709 	if (subtitle != NULL) {
1710 		gtk_tree_model_get (playlist->priv->model, &iter,
1711 				    URI_COL, &path,
1712 				    SUBTITLE_URI_COL, subtitle,
1713 				    -1);
1714 	} else {
1715 		gtk_tree_model_get (playlist->priv->model, &iter,
1716 				    URI_COL, &path,
1717 				    -1);
1718 	}
1719 
1720 	return path;
1721 }
1722 
1723 char *
totem_playlist_get_current_title(TotemPlaylist * playlist)1724 totem_playlist_get_current_title (TotemPlaylist *playlist)
1725 {
1726 	GtkTreeIter iter;
1727 	char *title;
1728 
1729 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1730 
1731 	if (update_current_from_playlist (playlist) == FALSE)
1732 		return NULL;
1733 
1734 	gtk_tree_model_get_iter (playlist->priv->model,
1735 				 &iter,
1736 				 playlist->priv->current);
1737 
1738 	gtk_tree_model_get (playlist->priv->model,
1739 			    &iter,
1740 			    FILENAME_COL, &title,
1741 			    -1);
1742 	return title;
1743 }
1744 
1745 char *
totem_playlist_get_current_content_type(TotemPlaylist * playlist)1746 totem_playlist_get_current_content_type (TotemPlaylist *playlist)
1747 {
1748 	GtkTreeIter iter;
1749 	char *content_type;
1750 
1751 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1752 
1753 	if (update_current_from_playlist (playlist) == FALSE)
1754 		return NULL;
1755 
1756 	gtk_tree_model_get_iter (playlist->priv->model,
1757 				 &iter,
1758 				 playlist->priv->current);
1759 
1760 	gtk_tree_model_get (playlist->priv->model,
1761 			    &iter,
1762 			    MIME_TYPE_COL, &content_type,
1763 			    -1);
1764 
1765 	return content_type;
1766 }
1767 
1768 gint64
totem_playlist_steal_current_starttime(TotemPlaylist * playlist)1769 totem_playlist_steal_current_starttime (TotemPlaylist *playlist)
1770 {
1771 	GtkTreeIter iter;
1772 	gint64 starttime;
1773 
1774 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), 0);
1775 
1776 	if (update_current_from_playlist (playlist) == FALSE)
1777 		return 0;
1778 
1779 	gtk_tree_model_get_iter (playlist->priv->model,
1780 				 &iter,
1781 				 playlist->priv->current);
1782 
1783 	gtk_tree_model_get (playlist->priv->model,
1784 			    &iter,
1785 			    STARTTIME_COL, &starttime,
1786 			    -1);
1787 
1788 	/* And reset the starttime so it's only used once,
1789 	 * hence the "steal" in the API name */
1790 	gtk_list_store_set (GTK_LIST_STORE (playlist->priv->model),
1791 			    &iter,
1792 			    STARTTIME_COL, 0,
1793 			    -1);
1794 
1795 	return starttime;
1796 }
1797 
1798 char *
totem_playlist_get_title(TotemPlaylist * playlist,guint title_index)1799 totem_playlist_get_title (TotemPlaylist *playlist, guint title_index)
1800 {
1801 	GtkTreeIter iter;
1802 	GtkTreePath *path;
1803 	char *title;
1804 
1805 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), NULL);
1806 
1807 	path = gtk_tree_path_new_from_indices (title_index, -1);
1808 
1809 	gtk_tree_model_get_iter (playlist->priv->model,
1810 				 &iter, path);
1811 
1812 	gtk_tree_path_free (path);
1813 
1814 	gtk_tree_model_get (playlist->priv->model,
1815 			    &iter,
1816 			    FILENAME_COL, &title,
1817 			    -1);
1818 
1819 	return title;
1820 }
1821 
1822 gboolean
totem_playlist_has_previous_mrl(TotemPlaylist * playlist)1823 totem_playlist_has_previous_mrl (TotemPlaylist *playlist)
1824 {
1825 	GtkTreeIter iter;
1826 
1827 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1828 
1829 	if (update_current_from_playlist (playlist) == FALSE)
1830 		return FALSE;
1831 
1832 	gtk_tree_model_get_iter (playlist->priv->model,
1833 				 &iter,
1834 				 playlist->priv->current);
1835 
1836 	return gtk_tree_model_iter_previous (playlist->priv->model, &iter);
1837 }
1838 
1839 gboolean
totem_playlist_has_next_mrl(TotemPlaylist * playlist)1840 totem_playlist_has_next_mrl (TotemPlaylist *playlist)
1841 {
1842 	GtkTreeIter iter;
1843 
1844 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1845 
1846 	if (update_current_from_playlist (playlist) == FALSE)
1847 		return FALSE;
1848 
1849 	gtk_tree_model_get_iter (playlist->priv->model,
1850 				 &iter,
1851 				 playlist->priv->current);
1852 
1853 	return gtk_tree_model_iter_next (playlist->priv->model, &iter);
1854 }
1855 
1856 gboolean
totem_playlist_set_title(TotemPlaylist * playlist,const char * title)1857 totem_playlist_set_title (TotemPlaylist *playlist, const char *title)
1858 {
1859 	GtkListStore *store;
1860 	GtkTreeIter iter;
1861 	char *escaped_title;
1862 
1863 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1864 
1865 	if (update_current_from_playlist (playlist) == FALSE)
1866 		return FALSE;
1867 
1868 	store = GTK_LIST_STORE (playlist->priv->model);
1869 	gtk_tree_model_get_iter (playlist->priv->model,
1870 			&iter,
1871 			playlist->priv->current);
1872 
1873 	escaped_title = g_markup_escape_text (title, -1);
1874 	gtk_list_store_set (store, &iter,
1875 			FILENAME_COL, title,
1876 			FILENAME_ESCAPED_COL, escaped_title,
1877 			TITLE_CUSTOM_COL, TRUE,
1878 			-1);
1879 	g_free (escaped_title);
1880 
1881 	g_signal_emit (playlist,
1882 		       totem_playlist_table_signals[ACTIVE_NAME_CHANGED], 0);
1883 
1884 	return TRUE;
1885 }
1886 
1887 gboolean
totem_playlist_set_playing(TotemPlaylist * playlist,TotemPlaylistStatus state)1888 totem_playlist_set_playing (TotemPlaylist *playlist, TotemPlaylistStatus state)
1889 {
1890 	GtkListStore *store;
1891 	GtkTreeIter iter;
1892 	GtkTreePath *path;
1893 
1894 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1895 
1896 	if (update_current_from_playlist (playlist) == FALSE)
1897 		return FALSE;
1898 
1899 	store = GTK_LIST_STORE (playlist->priv->model);
1900 	gtk_tree_model_get_iter (playlist->priv->model,
1901 			&iter,
1902 			playlist->priv->current);
1903 
1904 	gtk_list_store_set (store, &iter,
1905 			PLAYING_COL, state,
1906 			-1);
1907 
1908 	if (state == FALSE)
1909 		return TRUE;
1910 
1911 	path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
1912 	gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (playlist->priv->treeview),
1913 				      path, NULL,
1914 				      TRUE, 0.5, 0);
1915 	gtk_tree_path_free (path);
1916 
1917 	return TRUE;
1918 }
1919 
1920 TotemPlaylistStatus
totem_playlist_get_playing(TotemPlaylist * playlist)1921 totem_playlist_get_playing (TotemPlaylist *playlist)
1922 {
1923 	GtkTreeIter iter;
1924 	TotemPlaylistStatus status;
1925 
1926 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), TOTEM_PLAYLIST_STATUS_NONE);
1927 
1928 	if (gtk_tree_model_get_iter (playlist->priv->model, &iter, playlist->priv->current) == FALSE)
1929 		return TOTEM_PLAYLIST_STATUS_NONE;
1930 
1931 	gtk_tree_model_get (playlist->priv->model,
1932 			    &iter,
1933 			    PLAYING_COL, &status,
1934 			    -1);
1935 
1936 	return status;
1937 }
1938 
1939 void
totem_playlist_set_previous(TotemPlaylist * playlist)1940 totem_playlist_set_previous (TotemPlaylist *playlist)
1941 {
1942 	GtkTreeIter iter;
1943 	char *path;
1944 
1945 	g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
1946 
1947 	if (totem_playlist_has_previous_mrl (playlist) == FALSE)
1948 		return;
1949 
1950 	totem_playlist_unset_playing (playlist);
1951 
1952 	path = gtk_tree_path_to_string (playlist->priv->current);
1953 	if (g_str_equal (path, "0")) {
1954 		totem_playlist_set_at_end (playlist);
1955 		g_free (path);
1956 		return;
1957 	}
1958 	g_free (path);
1959 
1960 	gtk_tree_model_get_iter (playlist->priv->model,
1961 				 &iter,
1962 				 playlist->priv->current);
1963 
1964 	if (!gtk_tree_model_iter_previous (playlist->priv->model, &iter))
1965 		g_assert_not_reached ();
1966 	gtk_tree_path_free (playlist->priv->current);
1967 	playlist->priv->current = gtk_tree_model_get_path
1968 		(playlist->priv->model, &iter);
1969 }
1970 
1971 void
totem_playlist_set_next(TotemPlaylist * playlist)1972 totem_playlist_set_next (TotemPlaylist *playlist)
1973 {
1974 	GtkTreeIter iter;
1975 
1976 	g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
1977 
1978 	if (totem_playlist_has_next_mrl (playlist) == FALSE) {
1979 		totem_playlist_set_at_start (playlist);
1980 		return;
1981 	}
1982 
1983 	totem_playlist_unset_playing (playlist);
1984 
1985 	gtk_tree_model_get_iter (playlist->priv->model,
1986 				 &iter,
1987 				 playlist->priv->current);
1988 
1989 	if (!gtk_tree_model_iter_next (playlist->priv->model, &iter))
1990 		g_assert_not_reached ();
1991 	gtk_tree_path_free (playlist->priv->current);
1992 	playlist->priv->current = gtk_tree_model_get_path (playlist->priv->model, &iter);
1993 }
1994 
1995 gboolean
totem_playlist_get_repeat(TotemPlaylist * playlist)1996 totem_playlist_get_repeat (TotemPlaylist *playlist)
1997 {
1998 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), FALSE);
1999 
2000 	return playlist->priv->repeat;
2001 }
2002 
2003 void
totem_playlist_set_repeat(TotemPlaylist * playlist,gboolean repeat)2004 totem_playlist_set_repeat (TotemPlaylist *playlist, gboolean repeat)
2005 {
2006 	g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2007 
2008 	g_settings_set_boolean (playlist->priv->settings, "repeat", repeat);
2009 }
2010 
2011 void
totem_playlist_set_at_start(TotemPlaylist * playlist)2012 totem_playlist_set_at_start (TotemPlaylist *playlist)
2013 {
2014 	g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2015 
2016 	totem_playlist_unset_playing (playlist);
2017 	g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
2018 	update_current_from_playlist (playlist);
2019 }
2020 
2021 void
totem_playlist_set_at_end(TotemPlaylist * playlist)2022 totem_playlist_set_at_end (TotemPlaylist *playlist)
2023 {
2024 	int indice;
2025 
2026 	g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2027 
2028 	totem_playlist_unset_playing (playlist);
2029 	g_clear_pointer (&playlist->priv->current, gtk_tree_path_free);
2030 
2031 	if (PL_LEN) {
2032 		indice = PL_LEN - 1;
2033 		playlist->priv->current = gtk_tree_path_new_from_indices
2034 			(indice, -1);
2035 	}
2036 }
2037 
2038 int
totem_playlist_get_current(TotemPlaylist * playlist)2039 totem_playlist_get_current (TotemPlaylist *playlist)
2040 {
2041 	char *path;
2042 	double current_index;
2043 
2044 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), -1);
2045 
2046 	if (playlist->priv->current == NULL)
2047 		return -1;
2048 	path = gtk_tree_path_to_string (playlist->priv->current);
2049 	if (path == NULL)
2050 		return -1;
2051 
2052 	current_index = g_ascii_strtod (path, NULL);
2053 	g_free (path);
2054 
2055 	return current_index;
2056 }
2057 
2058 int
totem_playlist_get_last(TotemPlaylist * playlist)2059 totem_playlist_get_last (TotemPlaylist *playlist)
2060 {
2061 	guint len = PL_LEN;
2062 
2063 	g_return_val_if_fail (TOTEM_IS_PLAYLIST (playlist), -1);
2064 
2065 	if (len == 0)
2066 		return -1;
2067 
2068 	return len - 1;
2069 }
2070 
2071 void
totem_playlist_set_current(TotemPlaylist * playlist,guint current_index)2072 totem_playlist_set_current (TotemPlaylist *playlist, guint current_index)
2073 {
2074 	g_return_if_fail (TOTEM_IS_PLAYLIST (playlist));
2075 
2076 	if (current_index >= (guint) PL_LEN)
2077 		return;
2078 
2079 	totem_playlist_unset_playing (playlist);
2080 	gtk_tree_path_free (playlist->priv->current);
2081 	playlist->priv->current = gtk_tree_path_new_from_indices (current_index, -1);
2082 }
2083 
2084 static void
totem_playlist_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)2085 totem_playlist_set_property (GObject      *object,
2086 			     guint         property_id,
2087 			     const GValue *value,
2088 			     GParamSpec   *pspec)
2089 {
2090 	TotemPlaylist *playlist;
2091 
2092 	playlist = TOTEM_PLAYLIST (object);
2093 
2094 	switch (property_id) {
2095 	case PROP_REPEAT:
2096 		g_settings_set_boolean (playlist->priv->settings, "repeat", g_value_get_boolean (value));
2097 		break;
2098 	default:
2099 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2100 		break;
2101 	}
2102 }
2103 
2104 static void
totem_playlist_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)2105 totem_playlist_get_property (GObject    *object,
2106 			     guint       property_id,
2107 			     GValue     *value,
2108 			     GParamSpec *pspec)
2109 {
2110 	TotemPlaylist *playlist;
2111 
2112 	playlist = TOTEM_PLAYLIST (object);
2113 
2114 	switch (property_id) {
2115 	case PROP_REPEAT:
2116 		g_value_set_boolean (value, playlist->priv->repeat);
2117 		break;
2118 	default:
2119 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2120 		break;
2121 	}
2122 }
2123 
2124 static void
totem_playlist_class_init(TotemPlaylistClass * klass)2125 totem_playlist_class_init (TotemPlaylistClass *klass)
2126 {
2127 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
2128 
2129 	object_class->set_property = totem_playlist_set_property;
2130 	object_class->get_property = totem_playlist_get_property;
2131 	object_class->dispose = totem_playlist_dispose;
2132 
2133 	/* Signals */
2134 	totem_playlist_table_signals[CHANGED] =
2135 		g_signal_new ("changed",
2136 				G_TYPE_FROM_CLASS (klass),
2137 				G_SIGNAL_RUN_LAST,
2138 				G_STRUCT_OFFSET (TotemPlaylistClass, changed),
2139 				NULL, NULL,
2140 				g_cclosure_marshal_VOID__VOID,
2141 				G_TYPE_NONE, 0);
2142 	totem_playlist_table_signals[ITEM_ACTIVATED] =
2143 		g_signal_new ("item-activated",
2144 				G_TYPE_FROM_CLASS (klass),
2145 				G_SIGNAL_RUN_LAST,
2146 				G_STRUCT_OFFSET (TotemPlaylistClass, item_activated),
2147 				NULL, NULL,
2148 				g_cclosure_marshal_VOID__VOID,
2149 				G_TYPE_NONE, 0);
2150 	totem_playlist_table_signals[ACTIVE_NAME_CHANGED] =
2151 		g_signal_new ("active-name-changed",
2152 				G_TYPE_FROM_CLASS (klass),
2153 				G_SIGNAL_RUN_LAST,
2154 				G_STRUCT_OFFSET (TotemPlaylistClass, active_name_changed),
2155 				NULL, NULL,
2156 				g_cclosure_marshal_VOID__VOID,
2157 				G_TYPE_NONE, 0);
2158 	totem_playlist_table_signals[CURRENT_REMOVED] =
2159 		g_signal_new ("current-removed",
2160 				G_TYPE_FROM_CLASS (klass),
2161 				G_SIGNAL_RUN_LAST,
2162 				G_STRUCT_OFFSET (TotemPlaylistClass,
2163 						 current_removed),
2164 				NULL, NULL,
2165 				g_cclosure_marshal_VOID__VOID,
2166 				G_TYPE_NONE, 0);
2167 	totem_playlist_table_signals[SUBTITLE_CHANGED] =
2168 		g_signal_new ("subtitle-changed",
2169 			      G_TYPE_FROM_CLASS (klass),
2170 			      G_SIGNAL_RUN_LAST,
2171 			      G_STRUCT_OFFSET (TotemPlaylistClass,
2172 					       subtitle_changed),
2173 			      NULL, NULL,
2174 			      g_cclosure_marshal_VOID__VOID,
2175 			      G_TYPE_NONE, 0);
2176 	totem_playlist_table_signals[ITEM_ADDED] =
2177 		g_signal_new ("item-added",
2178 				G_TYPE_FROM_CLASS (klass),
2179 				G_SIGNAL_RUN_LAST,
2180 				G_STRUCT_OFFSET (TotemPlaylistClass,
2181 					item_added),
2182 				NULL, NULL,
2183 				g_cclosure_marshal_generic,
2184 				G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
2185 	totem_playlist_table_signals[ITEM_REMOVED] =
2186 		g_signal_new ("item-removed",
2187 				G_TYPE_FROM_CLASS (klass),
2188 				G_SIGNAL_RUN_LAST,
2189 				G_STRUCT_OFFSET (TotemPlaylistClass,
2190 					item_removed),
2191 				NULL, NULL,
2192 				g_cclosure_marshal_generic,
2193 				G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
2194 
2195 	g_object_class_install_property (object_class, PROP_REPEAT,
2196 					 g_param_spec_boolean ("repeat", "Repeat",
2197 							       "Whether repeat mode is enabled.", FALSE,
2198 							       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
2199 }
2200