Location | Tool | Test ID | Function | Issue |
---|---|---|---|---|
rb-playlist-source.c:830:2 | clang-analyzer | Access to field 'impl_save_contents_to_xml' results in a dereference of a null pointer (loaded from variable 'klass') | ||
rb-playlist-source.c:830:2 | clang-analyzer | Access to field 'impl_save_contents_to_xml' results in a dereference of a null pointer (loaded from variable 'klass') |
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Copyright (C) 2002 Jorn Baayen <jorn@nl.linux.org>
4 * Copyright (C) 2003 Colin Walters <walters@gnome.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * The Rhythmbox authors hereby grant permission for non-GPL compatible
12 * GStreamer plugins to be used and distributed together with GStreamer
13 * and Rhythmbox. This permission is above and beyond the permissions granted
14 * by the GPL license by which Rhythmbox is covered. If you modify this code
15 * you may extend this exception to your version of the code, but you are not
16 * obligated to do so. If you do not wish to do so, delete this exception
17 * statement from your version.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27 *
28 */
29
30 #include "config.h"
31
32 #include <unistd.h>
33 #include <string.h>
34
35 #include <libxml/tree.h>
36 #include <glib/gi18n.h>
37 #include <gtk/gtk.h>
38 #include <totem-pl-parser.h>
39
40 #include "rb-entry-view.h"
41 #include "rb-search-entry.h"
42 #include "rb-file-helpers.h"
43 #include "rb-dialog.h"
44 #include "rb-util.h"
45 #include "rb-playlist-source.h"
46 #include "rb-debug.h"
47 #include "rb-song-info.h"
48
49 #include "rb-playlist-xml.h"
50 #include "rb-static-playlist-source.h"
51 #include "rb-auto-playlist-source.h"
52
53 #include "rb-playlist-manager.h"
54
55 /**
56 * SECTION:rb-playlist-source
57 * @short_description: Base class for playlist sources
58 *
59 * This class provides some common infrastructure for playlist
60 * sources. A playlist, in this context, is a persistent user-defined
61 * selection from a set of songs. Playlists (static and auto) based
62 * on the main library are saved to the playlists.xml file stored
63 * alongside the rhythmdb.xml file. Playlists on portable music players
64 * are saved on the device in the format the player itself supports.
65 *
66 * This class provides most of the source UI (excluding the search bar),
67 * holds some of the framework for loading and saving the playlists.xml
68 * file, and records which playlists need to be saved.
69 */
70
71 static void rb_playlist_source_class_init (RBPlaylistSourceClass *klass);
72 static void rb_playlist_source_init (RBPlaylistSource *source);
73 static void rb_playlist_source_constructed (GObject *object);
74 static void rb_playlist_source_dispose (GObject *object);
75 static void rb_playlist_source_finalize (GObject *object);
76 static void rb_playlist_source_set_property (GObject *object,
77 guint prop_id,
78 const GValue *value,
79 GParamSpec *pspec);
80 static void rb_playlist_source_get_property (GObject *object,
81 guint prop_id,
82 GValue *value,
83 GParamSpec *pspec);
84
85 /* source methods */
86 static RBEntryView *impl_get_entry_view (RBSource *source);
87 static void impl_song_properties (RBSource *source);
88 static gboolean impl_show_popup (RBDisplayPage *page);
89
90 static void rb_playlist_source_songs_show_popup_cb (RBEntryView *view,
91 gboolean over_entry,
92 RBPlaylistSource *playlist_view);
93 static void rb_playlist_source_drop_cb (GtkWidget *widget,
94 GdkDragContext *context,
95 gint x,
96 gint y,
97 GtkSelectionData *data,
98 guint info,
99 guint time,
100 gpointer user_data);
101
102 static void rb_playlist_source_row_deleted (GtkTreeModel *model,
103 GtkTreePath *path,
104 RBPlaylistSource *playlist);
105 static void default_show_entry_view_popup (RBPlaylistSource *source,
106 RBEntryView *view,
107 gboolean over_entry);
108 static void rb_playlist_source_entry_added_cb (RhythmDB *db, RhythmDBEntry *entry,
109 RBPlaylistSource *source);
110 static void rb_playlist_source_track_cell_data_func (GtkTreeViewColumn *column, GtkCellRenderer *renderer,
111 GtkTreeModel *tree_model, GtkTreeIter *iter,
112 RBPlaylistSource *source);
113 static void default_mark_dirty (RBPlaylistSource *source);
114 static void rb_playlist_source_songs_sort_order_changed_cb (GObject *object,
115 GParamSpec *pspec,
116 RBPlaylistSource *source);
117
118 static void remove_from_playlist_cmd (GtkAction *action, RBSource *source);
119 static char *impl_get_delete_action (RBSource *source);
120
121 #define PLAYLIST_SOURCE_SONGS_POPUP_PATH "/PlaylistViewPopup"
122 #define PLAYLIST_SOURCE_POPUP_PATH "/PlaylistSourcePopup"
123
124 static GtkActionEntry rb_playlist_source_actions [] =
125 {
126 { "RemoveFromPlaylist", GTK_STOCK_REMOVE, N_("Remove From Playlist"), NULL,
127 N_("Remove each selected song from the playlist"),
128 G_CALLBACK (remove_from_playlist_cmd) },
129 };
130
131
132 struct RBPlaylistSourcePrivate
133 {
134 RhythmDB *db;
135 GtkActionGroup *action_group;
136
137 GHashTable *entries;
138
139 RhythmDBQueryModel *model;
140
141 RBEntryView *songs;
142
143 gboolean dirty;
144 gboolean is_local;
145 gboolean dispose_has_run;
146
147 char *title;
148 };
149
150 #define RB_PLAYLIST_SOURCE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_PLAYLIST_SOURCE, RBPlaylistSourcePrivate))
151
152 enum
153 {
154 PROP_0,
155 PROP_DB,
156 PROP_DIRTY,
157 PROP_LOCAL,
158 };
159
160 static const GtkTargetEntry target_uri [] = { { "text/uri-list", 0, 0 } };
161
162 G_DEFINE_ABSTRACT_TYPE (RBPlaylistSource, rb_playlist_source, RB_TYPE_SOURCE);
163
164 static void
165 rb_playlist_source_class_init (RBPlaylistSourceClass *klass)
166 {
167 GObjectClass *object_class = G_OBJECT_CLASS (klass);
168 RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
169 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
170
171 object_class->dispose = rb_playlist_source_dispose;
172 object_class->finalize = rb_playlist_source_finalize;
173 object_class->constructed = rb_playlist_source_constructed;
174 object_class->set_property = rb_playlist_source_set_property;
175 object_class->get_property = rb_playlist_source_get_property;
176
177 page_class->show_popup = impl_show_popup;
178
179 source_class->impl_get_entry_view = impl_get_entry_view;
180 source_class->impl_can_rename = (RBSourceFeatureFunc) rb_true_function;
181 source_class->impl_can_cut = (RBSourceFeatureFunc) rb_false_function;
182 source_class->impl_can_copy = (RBSourceFeatureFunc) rb_true_function;
183 source_class->impl_can_delete = (RBSourceFeatureFunc) rb_false_function;
184 source_class->impl_can_add_to_queue = (RBSourceFeatureFunc) rb_true_function;
185 source_class->impl_can_move_to_trash = (RBSourceFeatureFunc) rb_true_function;
186 source_class->impl_song_properties = impl_song_properties;
187 source_class->impl_get_delete_action = impl_get_delete_action;
188
189 klass->impl_show_entry_view_popup = default_show_entry_view_popup;
190 klass->impl_mark_dirty = default_mark_dirty;
191
192 /**
193 * RBPlaylistSource:db:
194 *
195 * The #RhythmDB instance
196 */
197 g_object_class_install_property (object_class,
198 PROP_DB,
199 g_param_spec_object ("db",
200 "db",
201 "rhythmdb instance",
202 RHYTHMDB_TYPE,
203 G_PARAM_READABLE));
204 /**
205 * RBPlaylistSource:dirty:
206 *
207 * Whether the playlist has been changed since it was last saved
208 * to disk.
209 */
210 g_object_class_install_property (object_class,
211 PROP_DIRTY,
212 g_param_spec_boolean ("dirty",
213 "dirty",
214 "whether this playlist should be saved",
215 FALSE,
216 G_PARAM_READABLE));
217 /**
218 * RBPlaylistSource:is-local:
219 *
220 * Whether the playlist is attached to the local library.
221 * Remote DAAP playlists, for example, are not local.
222 */
223 g_object_class_install_property (object_class,
224 PROP_LOCAL,
225 g_param_spec_boolean ("is-local",
226 "is-local",
227 "whether this playlist is attached to the local library",
228 TRUE,
229 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
230
231 g_type_class_add_private (klass, sizeof (RBPlaylistSourcePrivate));
232 }
233
234 static void
235 rb_playlist_source_init (RBPlaylistSource *source)
236 {
237 source->priv = RB_PLAYLIST_SOURCE_GET_PRIVATE (source);
238 }
239
240 static void
241 rb_playlist_source_set_db (RBPlaylistSource *source,
242 RhythmDB *db)
243 {
244 if (source->priv->db != NULL) {
245 g_signal_handlers_disconnect_by_func (source->priv->db,
246 rb_playlist_source_entry_added_cb,
247 source);
248 g_object_unref (source->priv->db);
249
250 }
251
252 source->priv->db = db;
253
254 if (source->priv->db != NULL) {
255 g_object_ref (source->priv->db);
256 g_signal_connect_object (G_OBJECT (source->priv->db), "entry_added",
257 G_CALLBACK (rb_playlist_source_entry_added_cb),
258 source, 0);
259 }
260
261 }
262
263 static void
264 rb_playlist_source_constructed (GObject *object)
265 {
266 GObject *shell_player;
267 RBPlaylistSource *source;
268 RBShell *shell;
269 RhythmDB *db;
270 RhythmDBQueryModel *query_model;
271
272 RB_CHAIN_GOBJECT_METHOD (rb_playlist_source_parent_class, constructed, object);
273 source = RB_PLAYLIST_SOURCE (object);
274
275 g_object_get (source, "shell", &shell, NULL);
276 g_object_get (shell,
277 "db", &db,
278 "shell-player", &shell_player,
279 NULL);
280 rb_playlist_source_set_db (source, db);
281 g_object_unref (db);
282
283 source->priv->action_group = _rb_display_page_register_action_group (RB_DISPLAY_PAGE (source),
284 "PlaylistActions",
285 NULL, 0,
286 shell);
287 _rb_action_group_add_display_page_actions (source->priv->action_group,
288 G_OBJECT (shell),
289 rb_playlist_source_actions,
290 G_N_ELEMENTS (rb_playlist_source_actions));
291
292 g_object_unref (shell);
293
294 source->priv->entries = g_hash_table_new_full (rb_refstring_hash, rb_refstring_equal,
295 (GDestroyNotify)rb_refstring_unref, NULL);
296
297 source->priv->songs = rb_entry_view_new (source->priv->db,
298 shell_player,
299 TRUE, TRUE);
300 g_object_unref (shell_player);
301
302 g_signal_connect_object (source->priv->songs,
303 "notify::sort-order",
304 G_CALLBACK (rb_playlist_source_songs_sort_order_changed_cb),
305 source, 0);
306
307 query_model = rhythmdb_query_model_new_empty (source->priv->db);
308 rb_playlist_source_set_query_model (source, query_model);
309 g_object_unref (query_model);
310
311 {
312 const char *title = "";
313 const char *strings[3] = {0};
314
315 GtkTreeViewColumn *column = gtk_tree_view_column_new ();
316 GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
317
318 g_object_set(renderer,
319 "style", PANGO_STYLE_OBLIQUE,
320 "weight", PANGO_WEIGHT_LIGHT,
321 "xalign", 1.0,
322 NULL);
323
324 gtk_tree_view_column_pack_start (column, renderer, TRUE);
325
326 gtk_tree_view_column_set_resizable (column, TRUE);
327 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
328
329 strings[0] = title;
330 strings[1] = "9999";
331 rb_entry_view_set_fixed_column_width (source->priv->songs, column, renderer,
332 strings);
333 gtk_tree_view_column_set_cell_data_func (column, renderer,
334 (GtkTreeCellDataFunc)
335 rb_playlist_source_track_cell_data_func,
336 source, NULL);
337 rb_entry_view_insert_column_custom (source->priv->songs, column, title,
338 "PlaylistTrack", NULL, 0, NULL, 0);
339 }
340
341 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_TRACK_NUMBER, FALSE);
342 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_TITLE, TRUE);
343 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_GENRE, FALSE);
344 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
345 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
346 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_YEAR, FALSE);
347 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_DURATION, FALSE);
348 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_QUALITY, FALSE);
349 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_RATING, FALSE);
350 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_PLAY_COUNT, FALSE);
351 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_COMMENT, FALSE);
352 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_LOCATION, FALSE);
353 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_LAST_PLAYED, FALSE);
354 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_FIRST_SEEN, FALSE);
355 rb_entry_view_append_column (source->priv->songs, RB_ENTRY_VIEW_COL_BPM, FALSE);
356 rb_entry_view_set_columns_clickable (source->priv->songs, FALSE);
357
358 rb_playlist_source_setup_entry_view (source, source->priv->songs);
359
360 gtk_container_add (GTK_CONTAINER (source), GTK_WIDGET (source->priv->songs));
361
362 gtk_widget_show_all (GTK_WIDGET (source));
363 }
364
365 static void
366 rb_playlist_source_dispose (GObject *object)
367 {
368 RBPlaylistSource *source = RB_PLAYLIST_SOURCE (object);
369
370 if (source->priv->dispose_has_run) {
371 /* If dispose did already run, return. */
372 rb_debug ("Dispose has already run for playlist source %p", object);
373 return;
374 }
375 /* Make sure dispose does not run twice. */
376 source->priv->dispose_has_run = TRUE;
377
378 rb_debug ("Disposing playlist source %p", source);
379
380 if (source->priv->db != NULL) {
381 g_object_unref (source->priv->db);
382 source->priv->db = NULL;
383 }
384
385 if (source->priv->model != NULL) {
386 g_object_unref (source->priv->model);
387 source->priv->model = NULL;
388 }
389
390 G_OBJECT_CLASS (rb_playlist_source_parent_class)->dispose (object);
391 }
392
393 static void
394 rb_playlist_source_finalize (GObject *object)
395 {
396 RBPlaylistSource *source;
397
398 g_return_if_fail (object != NULL);
399 g_return_if_fail (RB_IS_PLAYLIST_SOURCE (object));
400
401 source = RB_PLAYLIST_SOURCE (object);
402 g_return_if_fail (source->priv != NULL);
403
404 rb_debug ("Finalizing playlist source %p", source);
405
406 g_hash_table_destroy (source->priv->entries);
407
408 g_free (source->priv->title);
409 source->priv = NULL;
410
411 G_OBJECT_CLASS (rb_playlist_source_parent_class)->finalize (object);
412 }
413
414 static void
415 rb_playlist_source_set_property (GObject *object,
416 guint prop_id,
417 const GValue *value,
418 GParamSpec *pspec)
419 {
420 RBPlaylistSource *source = RB_PLAYLIST_SOURCE (object);
421
422 switch (prop_id) {
423 case PROP_LOCAL:
424 source->priv->is_local = g_value_get_boolean (value);
425 break;
426 default:
427 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
428 break;
429 }
430 }
431
432 static void
433 rb_playlist_source_get_property (GObject *object,
434 guint prop_id,
435 GValue *value,
436 GParamSpec *pspec)
437 {
438 RBPlaylistSource *source = RB_PLAYLIST_SOURCE (object);
439
440 switch (prop_id) {
441 case PROP_DB:
442 g_value_set_object (value, source->priv->db);
443 break;
444 case PROP_DIRTY:
445 g_value_set_boolean (value, source->priv->dirty);
446 break;
447 case PROP_LOCAL:
448 g_value_set_boolean (value, source->priv->is_local);
449 break;
450 default:
451 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
452 break;
453 }
454 }
455
456 static void
457 default_show_entry_view_popup (RBPlaylistSource *source,
458 RBEntryView *view,
459 gboolean over_entry)
460 {
461 if (over_entry) {
462 _rb_display_page_show_popup (RB_DISPLAY_PAGE (source), PLAYLIST_SOURCE_SONGS_POPUP_PATH);
463 }
464 }
465
466 static void
467 rb_playlist_source_songs_show_popup_cb (RBEntryView *view,
468 gboolean over_entry,
469 RBPlaylistSource *source)
470 {
471 RBPlaylistSourceClass *klass = RB_PLAYLIST_SOURCE_GET_CLASS (source);
472 if (klass->impl_show_entry_view_popup)
473 klass->impl_show_entry_view_popup (source, view, over_entry);
474 }
475
476 static RBEntryView *
477 impl_get_entry_view (RBSource *asource)
478 {
479 RBPlaylistSource *source = RB_PLAYLIST_SOURCE (asource);
480
481 return source->priv->songs;
482 }
483
484 static void
485 impl_song_properties (RBSource *asource)
486 {
487 RBPlaylistSource *source = RB_PLAYLIST_SOURCE (asource);
488 GtkWidget *song_info = NULL;
489
490 g_return_if_fail (source->priv->songs != NULL);
491
492 song_info = rb_song_info_new (asource, NULL);
493 if (song_info)
494 gtk_widget_show_all (song_info);
495 else
496 rb_debug ("failed to create dialog, or no selection!");
497 }
498
499 static gboolean
500 impl_show_popup (RBDisplayPage *page)
501 {
502 _rb_display_page_show_popup (page, PLAYLIST_SOURCE_POPUP_PATH);
503 return TRUE;
504 }
505
506 static void
507 rb_playlist_source_drop_cb (GtkWidget *widget,
508 GdkDragContext *context,
509 gint x,
510 gint y,
511 GtkSelectionData *data,
512 guint info,
513 guint time,
514 gpointer user_data)
515 {
516 RBPlaylistSource *source = RB_PLAYLIST_SOURCE (user_data);
517 GtkTargetList *tlist;
518 GdkAtom target;
519
520 tlist = gtk_target_list_new (target_uri, G_N_ELEMENTS (target_uri));
521 target = gtk_drag_dest_find_target (widget, context, tlist);
522 gtk_target_list_unref (tlist);
523
524 if (target == GDK_NONE)
525 return;
526
527 rb_display_page_receive_drag (RB_DISPLAY_PAGE (source), data);
528
529 gtk_drag_finish (context, TRUE, FALSE, time);
530 }
531
532 #if TOTEM_PL_PARSER_CHECK_VERSION(2,29,1)
533
534 static void
535 set_field_from_property (TotemPlPlaylist *playlist,
536 TotemPlPlaylistIter *iter,
537 RhythmDBEntry *entry,
538 RhythmDBPropType property,
539 const char *field)
540 {
541 const char *value;
542
543 value = rhythmdb_entry_get_string (entry, property);
544 if (value != NULL) {
545 totem_pl_playlist_set (playlist, iter, field, value, NULL);
546 }
547 }
548
549 static gboolean
550 playlist_iter_foreach (GtkTreeModel *model,
551 GtkTreePath *path,
552 GtkTreeIter *iter,
553 TotemPlPlaylist *playlist)
554 {
555 TotemPlPlaylistIter pl_iter;
556 RhythmDBEntry *entry;
557
558 gtk_tree_model_get (model, iter, 0, &entry, -1);
559 if (entry == NULL) {
560 return FALSE;
561 }
562
563 totem_pl_playlist_append (playlist, &pl_iter);
564 set_field_from_property (playlist, &pl_iter, entry, RHYTHMDB_PROP_LOCATION, TOTEM_PL_PARSER_FIELD_URI);
565 set_field_from_property (playlist, &pl_iter, entry, RHYTHMDB_PROP_ARTIST, TOTEM_PL_PARSER_FIELD_AUTHOR);
566 set_field_from_property (playlist, &pl_iter, entry, RHYTHMDB_PROP_GENRE, TOTEM_PL_PARSER_FIELD_GENRE);
567 set_field_from_property (playlist, &pl_iter, entry, RHYTHMDB_PROP_ALBUM, TOTEM_PL_PARSER_FIELD_ALBUM);
568 set_field_from_property (playlist, &pl_iter, entry, RHYTHMDB_PROP_TITLE, TOTEM_PL_PARSER_FIELD_TITLE);
569
570 /* could possibly set duration, file size.. ? */
571
572 return FALSE;
573 }
574
575
576 #else
577
578 static void
579 playlist_iter_func (GtkTreeModel *model,
580 GtkTreeIter *iter,
581 char **uri,
582 char **title,
583 gboolean *custom_title,
584 gpointer user_data)
585 {
586 RhythmDBEntry *entry;
587
588 gtk_tree_model_get (model, iter, 0, &entry, -1);
589
590 if (uri != NULL) {
591 *uri = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_LOCATION);
592 }
593 if (title != NULL) {
594 *title = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_TITLE);
595 }
596 if (custom_title != NULL) {
597 *custom_title = TRUE;
598 }
599
600 if (entry != NULL) {
601 rhythmdb_entry_unref (entry);
602 }
603 }
604
605 #endif
606
607 /**
608 * rb_playlist_source_save_playlist:
609 * @source: a #RBPlaylistSource
610 * @uri: destination URI
611 * @export_type: format to save in
612 *
613 * Saves the playlist to an external file in a standard
614 * format (M3U, PLS, or XSPF).
615 */
616 void
617 rb_playlist_source_save_playlist (RBPlaylistSource *source,
618 const char *uri,
619 RBPlaylistExportType export_type)
620 {
621 TotemPlParser *pl;
622 GError *error = NULL;
623 char *name;
624 gint totem_format;
625 #if TOTEM_PL_PARSER_CHECK_VERSION(2,29,1)
626 TotemPlPlaylist *playlist;
627 GFile *file;
628 #endif
629
630 g_return_if_fail (RB_IS_PLAYLIST_SOURCE (source));
631
632 rb_debug ("saving playlist");
633 pl = totem_pl_parser_new ();
634
635 g_object_get (source, "name", &name, NULL);
636
637 switch (export_type) {
638 case RB_PLAYLIST_EXPORT_TYPE_XSPF:
639 totem_format = TOTEM_PL_PARSER_XSPF;
640 break;
641 case RB_PLAYLIST_EXPORT_TYPE_M3U:
642 totem_format = TOTEM_PL_PARSER_M3U;
643 break;
644 case RB_PLAYLIST_EXPORT_TYPE_PLS:
645 default:
646 totem_format = TOTEM_PL_PARSER_PLS;
647 break;
648 }
649
650 #if TOTEM_PL_PARSER_CHECK_VERSION(2,29,1)
651 file = g_file_new_for_uri (uri);
652 playlist = totem_pl_playlist_new ();
653
654 gtk_tree_model_foreach (GTK_TREE_MODEL (source->priv->model),
655 (GtkTreeModelForeachFunc)playlist_iter_foreach,
656 playlist);
657 totem_pl_parser_save (pl, playlist, file, name, totem_format, &error);
658 g_object_unref (playlist);
659 g_object_unref (file);
660 #else
661 totem_pl_parser_write_with_title (pl, GTK_TREE_MODEL (source->priv->model),
662 playlist_iter_func, uri, name,
663 totem_format,
664 NULL, &error);
665 #endif
666 g_object_unref (pl);
667 g_free (name);
668 if (error != NULL) {
669 rb_error_dialog (NULL, _("Couldn't save playlist"),
670 "%s", error->message);
671 g_error_free (error);
672 }
673 }
674
675 /* Adapted from yelp-toc-pager.c */
676 static xmlChar *
677 xml_get_and_trim_names (xmlNodePtr node)
678 {
679 xmlNodePtr cur;
680 xmlChar *keep_lang = NULL;
681 xmlChar *value;
682 int j, keep_pri = INT_MAX;
683
684 const gchar * const * langs = g_get_language_names ();
685
686 value = NULL;
687
688 for (cur = node->children; cur; cur = cur->next) {
689 if (! xmlStrcmp (cur->name, RB_PLAYLIST_NAME)) {
690 xmlChar *cur_lang = NULL;
691 int cur_pri = INT_MAX;
692
693 cur_lang = xmlNodeGetLang (cur);
694
695 if (cur_lang) {
696 for (j = 0; langs[j]; j++) {
697 if (g_str_equal (cur_lang, langs[j])) {
698 cur_pri = j;
699 break;
700 }
701 }
702 } else {
703 cur_pri = INT_MAX - 1;
704 }
705
706 if (cur_pri <= keep_pri) {
707 if (keep_lang)
708 xmlFree (keep_lang);
709 if (value)
710 xmlFree (value);
711
712 value = xmlNodeGetContent (cur);
713
714 keep_lang = cur_lang;
715 keep_pri = cur_pri;
716 } else {
717 if (cur_lang)
718 xmlFree (cur_lang);
719 }
720 }
721 }
722
723 /* Delete all RB_PLAYLIST_NAME nodes */
724 cur = node->children;
725 while (cur) {
726 xmlNodePtr this = cur;
727 cur = cur->next;
728 if (! xmlStrcmp (this->name, RB_PLAYLIST_NAME)) {
729 xmlUnlinkNode (this);
730 xmlFreeNode (this);
731 }
732 }
733
734 return value;
735 }
736
737 static xmlChar *
738 get_playlist_name_from_xml (xmlNodePtr node)
739 {
740 xmlChar *name;
741
742 /* try to get and trim elements */
743 name = xml_get_and_trim_names (node);
744
745 if (name != NULL) {
746 return name;
747 }
748
749 /* try the attribute */
750 name = xmlGetProp (node, RB_PLAYLIST_NAME);
751
752 return name;
753 }
754
755 /**
756 * rb_playlist_source_new_from_xml:
757 * @shell: the #RBShell instance
758 * @node: libxml node containing the playlist
759 *
760 * Constructs a playlist source instance from the XML serialized
761 * format. This function knows about all the playlist types that
762 * can be saved to disk, and it hands off the XML node to the
763 * appropriate constructor based on the 'type' attribute of
764 * the root node of the playlist.
765 *
766 * Return value: the playlist
767 */
768 RBSource *
769 rb_playlist_source_new_from_xml (RBShell *shell,
770 xmlNodePtr node)
771 {
772 RBSource *source = NULL;
773 xmlChar *tmp;
774 xmlChar *name;
775
776 g_return_val_if_fail (RB_IS_SHELL (shell), NULL);
777
778 /* Try to get name from XML and remove translated names */
779 name = get_playlist_name_from_xml (node);
780
781 tmp = xmlGetProp (node, RB_PLAYLIST_TYPE);
782
783 if (!xmlStrcmp (tmp, RB_PLAYLIST_AUTOMATIC))
784 source = rb_auto_playlist_source_new_from_xml (shell, node);
785 else if (!xmlStrcmp (tmp, RB_PLAYLIST_STATIC))
786 source = rb_static_playlist_source_new_from_xml (shell, node);
787 else if (!xmlStrcmp (tmp, RB_PLAYLIST_QUEUE)) {
788 RBStaticPlaylistSource *queue;
789
790 g_object_get (shell, "queue-source", &queue, NULL);
791 rb_static_playlist_source_load_from_xml (queue, node);
792 g_object_unref (queue);
793 } else {
794 g_warning ("attempting to load playlist '%s' of unknown type '%s'", name, tmp);
795 }
796
797 if (source != NULL) {
798 g_object_set (G_OBJECT (source), "name", name, NULL);
799 }
800
801 xmlFree (name);
802 xmlFree (tmp);
803
804 return source;
805 }
806
807 /**
808 * rb_playlist_source_save_to_xml:
809 * @source: the playlist source to save
810 * @parent_node: libxml node below which to save the playlist
811 *
812 * Converts the playlist to XML format, below the specified
813 * parent node.
814 */
815 void
816 rb_playlist_source_save_to_xml (RBPlaylistSource *source,
817 xmlNodePtr parent_node)
818 {
819 xmlNodePtr node;
820 xmlChar *name;
821 RBPlaylistSourceClass *klass = RB_PLAYLIST_SOURCE_GET_CLASS (source);
822
823 g_return_if_fail (RB_IS_PLAYLIST_SOURCE (source));
824
825 node = xmlNewChild (parent_node, NULL, RB_PLAYLIST_PLAYLIST, NULL);
826 g_object_get (source, "name", &name, NULL);
827 xmlSetProp (node, RB_PLAYLIST_NAME, name);
828 g_free (name);
829
830 klass->impl_save_contents_to_xml (source, node);
(emitted by clang-analyzer)TODO: a detailed trace is available in the data model (not yet rendered in this report)
(emitted by clang-analyzer)TODO: a detailed trace is available in the data model (not yet rendered in this report)
831
832 source->priv->dirty = FALSE;
833 }
834
835 static void
836 rb_playlist_source_row_deleted (GtkTreeModel *model,
837 GtkTreePath *path,
838 RBPlaylistSource *source)
839 {
840 RhythmDBEntry *entry;
841 RBRefString *location;
842
843 entry = rhythmdb_query_model_tree_path_to_entry (RHYTHMDB_QUERY_MODEL (model),
844 path);
845
846 location = rhythmdb_entry_get_refstring (entry, RHYTHMDB_PROP_LOCATION);
847 if (g_hash_table_remove (source->priv->entries, location))
848 source->priv->dirty = TRUE;
849
850 rb_refstring_unref (location);
851 rhythmdb_entry_unref (entry);
852 }
853
854 static void
855 rb_playlist_source_entry_added_cb (RhythmDB *db,
856 RhythmDBEntry *entry,
857 RBPlaylistSource *source)
858 {
859 RBRefString *location;
860
861 location = rhythmdb_entry_get_refstring (entry, RHYTHMDB_PROP_LOCATION);
862
863 if (g_hash_table_lookup (source->priv->entries, location)) {
864 if (_rb_source_check_entry_type (RB_SOURCE (source), entry)) {
865 rhythmdb_query_model_add_entry (source->priv->model, entry, -1);
866 source->priv->dirty = TRUE;
867 } else {
868 g_hash_table_remove (source->priv->entries, location);
869 }
870 }
871
872 rb_refstring_unref (location);
873 }
874
875 static void
876 rb_playlist_source_track_cell_data_func (GtkTreeViewColumn *column,
877 GtkCellRenderer *renderer,
878 GtkTreeModel *tree_model,
879 GtkTreeIter *iter,
880 RBPlaylistSource *source)
881 {
882 char *str;
883 int val;
884
885 gtk_tree_model_get (tree_model, iter, 1, &val, -1);
886
887 if (val >= 0)
888 str = g_strdup_printf ("%d", val);
889 else
890 str = g_strdup ("");
891
892 g_object_set (G_OBJECT (renderer), "text", str, NULL);
893 g_free (str);
894 }
895
896 /**
897 * rb_playlist_source_setup_entry_view:
898 * @source: the #RBPlaylistSource
899 * @entry_view: the new #RBEntryView to set up
900 *
901 * Connects signal handlers and sets up drag and drop support for
902 * an entry view to be used by a playlist source. This only needs
903 * to be called if the playlist subclass is creating a new entry view.
904 */
905 void
906 rb_playlist_source_setup_entry_view (RBPlaylistSource *source,
907 RBEntryView *entry_view)
908 {
909 g_return_if_fail (RB_IS_PLAYLIST_SOURCE (source));
910
911 g_signal_connect_object (entry_view, "show_popup",
912 G_CALLBACK (rb_playlist_source_songs_show_popup_cb), source, 0);
913 g_signal_connect_object (entry_view, "drag_data_received",
914 G_CALLBACK (rb_playlist_source_drop_cb), source, 0);
915 gtk_drag_dest_set (GTK_WIDGET (entry_view),
916 GTK_DEST_DEFAULT_ALL,
917 target_uri,
918 G_N_ELEMENTS (target_uri),
919 GDK_ACTION_COPY);
920 }
921
922 /**
923 * rb_playlist_source_set_query_model:
924 * @source: the #RBPlaylistSource
925 * @model: the new #RhythmDBQueryModel
926 *
927 * Sets a new query model for the playlist. This updates the
928 * entry view to use the new query model and also updates the
929 * source query-model property.
930 *
931 * This needs to be called when the playlist subclass
932 * creates a new query model.
933 */
934 void
935 rb_playlist_source_set_query_model (RBPlaylistSource *source,
936 RhythmDBQueryModel *model)
937 {
938 g_return_if_fail (RB_IS_PLAYLIST_SOURCE (source));
939
940 if (source->priv->model != NULL) {
941 /* if the query model is replaced, the set of entries in
942 * the playlist will change, so we should mark the playlist dirty.
943 */
944 source->priv->dirty = TRUE;
945 g_signal_handlers_disconnect_by_func (source->priv->model,
946 G_CALLBACK (rb_playlist_source_row_deleted),
947 source);
948 g_object_unref (source->priv->model);
949 }
950
951 source->priv->model = model;
952
953 if (source->priv->model != NULL) {
954 g_object_ref (source->priv->model);
955 g_signal_connect_object (source->priv->model, "row_deleted",
956 G_CALLBACK (rb_playlist_source_row_deleted), source, 0);
957 }
958
959 rb_entry_view_set_model (source->priv->songs, RHYTHMDB_QUERY_MODEL (source->priv->model));
960
961 g_object_set (source, "query-model", source->priv->model, NULL);
962 }
963
964 /**
965 * rb_playlist_source_get_db:
966 * @source: a #RBPlaylistSource
967 *
968 * Returns the #RhythmDB instance. The caller must not
969 * unref the object once finished with it.
970 *
971 * Return value: (transfer none): the #RhythmDB instance
972 */
973 RhythmDB *
974 rb_playlist_source_get_db (RBPlaylistSource *source)
975 {
976 g_return_val_if_fail (RB_IS_PLAYLIST_SOURCE (source), NULL);
977
978 return source->priv->db;
979 }
980
981 /**
982 * rb_playlist_source_get_query_model:
983 * @source: a #RBPlaylistSource
984 *
985 * Returns the current #RhythmDBQueryModel for the playlist.
986 * The caller must not unref the object once finished with it.
987 *
988 * Return value: (transfer none): the current #RhythmDBQueryModel
989 */
990 RhythmDBQueryModel *
991 rb_playlist_source_get_query_model (RBPlaylistSource *source)
992 {
993 g_return_val_if_fail (RB_IS_PLAYLIST_SOURCE (source), NULL);
994
995 return source->priv->model;
996 }
997
998 static void
999 default_mark_dirty (RBPlaylistSource *source)
1000 {
1001 source->priv->dirty = TRUE;
1002 }
1003
1004 /**
1005 * rb_playlist_source_mark_dirty:
1006 * @source: a #RBPlaylistSource
1007 *
1008 * Marks the playlist dirty. This generally means that the playlist
1009 * will be saved to disk fairly soon, but the exact meaning can vary
1010 * between playlist types.
1011 */
1012 void
1013 rb_playlist_source_mark_dirty (RBPlaylistSource *source)
1014 {
1015 RBPlaylistSourceClass *klass;
1016 g_return_if_fail (RB_IS_PLAYLIST_SOURCE (source));
1017
1018 klass = RB_PLAYLIST_SOURCE_GET_CLASS (source);
1019 klass->impl_mark_dirty (source);
1020 g_object_notify (G_OBJECT (source), "dirty");
1021 }
1022
1023 /**
1024 * rb_playlist_source_location_in_map:
1025 * @source: a #RBPlaylistSource
1026 * @location: a URI to check
1027 *
1028 * Returns TRUE if the specified URI is in the playlist entry map
1029 *
1030 * Return value: %TRUE if the URI is present
1031 */
1032 gboolean
1033 rb_playlist_source_location_in_map (RBPlaylistSource *source,
1034 const char *location)
1035 {
1036 RBRefString *refstr;
1037 gboolean found;
1038
1039 g_return_val_if_fail (RB_IS_PLAYLIST_SOURCE (source), FALSE);
1040
1041 refstr = rb_refstring_find (location);
1042 if (refstr == NULL) {
1043 return FALSE;
1044 }
1045
1046 found = (g_hash_table_lookup (source->priv->entries, refstr) != NULL);
1047 rb_refstring_unref (refstr);
1048
1049 return found;
1050 }
1051
1052 /**
1053 * rb_playlist_source_add_to_map:
1054 * @source: a #RBPlaylistSource
1055 * @location: a URI to add
1056 *
1057 * Adds a URI to the playlist's entry map. This is useful when the
1058 * URI is being added to the database, but no entry exists for it yet.
1059 * When the entry is created, it will be added to the query model.
1060 *
1061 * Return value: TRUE if the URI was added to the entry map,
1062 * FALSE if it was already there.
1063 */
1064 gboolean
1065 rb_playlist_source_add_to_map (RBPlaylistSource *source,
1066 const char *location)
1067 {
1068 RBRefString *refstr;
1069
1070 g_return_val_if_fail (RB_IS_PLAYLIST_SOURCE (source), FALSE);
1071
1072 refstr = rb_refstring_new (location);
1073 if (g_hash_table_lookup (source->priv->entries, refstr)) {
1074 rb_refstring_unref (refstr);
1075 return FALSE;
1076 }
1077
1078 g_hash_table_insert (source->priv->entries,
1079 refstr, GINT_TO_POINTER (1));
1080
1081 return TRUE;
1082 }
1083
1084 static void
1085 rb_playlist_source_songs_sort_order_changed_cb (GObject *object,
1086 GParamSpec *pspec,
1087 RBPlaylistSource *source)
1088 {
1089 rb_debug ("sort order changed");
1090 rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
1091 }
1092
1093 static void
1094 remove_from_playlist_cmd (GtkAction *action, RBSource *source)
1095 {
1096 rb_source_delete (source);
1097 }
1098
1099 static char *
1100 impl_get_delete_action (RBSource *source)
1101 {
1102 return g_strdup ("RemoveFromPlaylist");
1103 }