No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2007 Jonathan Matthew <jonathan@d14n.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * The Rhythmbox authors hereby grant permission for non-GPL compatible
11 * GStreamer plugins to be used and distributed together with GStreamer
12 * and Rhythmbox. This permission is above and beyond the permissions granted
13 * by the GPL license by which Rhythmbox is covered. If you modify this code
14 * you may extend this exception to your version of the code, but you are not
15 * obligated to do so. If you do not wish to do so, delete this exception
16 * statement from your version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
26 *
27 */
28
29 #include "config.h"
30
31 #include <glib/gi18n.h>
32 #include <gtk/gtk.h>
33 #include <totem-pl-parser.h>
34
35 #include "rb-generic-player-playlist-source.h"
36 #include "rb-generic-player-source.h"
37 #include "rb-debug.h"
38 #include "rb-file-helpers.h"
39 #include "rb-util.h"
40
41 #define PLAYLIST_SAVE_TIMEOUT 1
42
43 typedef struct
44 {
45 char *playlist_path; /* hmm, replace with GFile? */
46 char *device_root;
47 gint save_playlist_id;
48 RBGenericPlayerSource *player_source;
49 gboolean loading;
50 } RBGenericPlayerPlaylistSourcePrivate;
51
52 G_DEFINE_DYNAMIC_TYPE(RBGenericPlayerPlaylistSource,
53 rb_generic_player_playlist_source,
54 RB_TYPE_STATIC_PLAYLIST_SOURCE)
55
56 #define GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_GENERIC_PLAYER_PLAYLIST_SOURCE, RBGenericPlayerPlaylistSourcePrivate))
57
58 enum {
59 PROP_0,
60 PROP_PLAYLIST_PATH,
61 PROP_DEVICE_ROOT,
62 PROP_PLAYER_SOURCE
63 };
64
65 typedef struct {
66 RBGenericPlayerPlaylistSource *source;
67 #if TOTEM_PL_PARSER_CHECK_VERSION(2,29,1)
68 TotemPlPlaylist *playlist;
69 #endif
70 TotemPlParserType playlist_type;
71 } SavePlaylistData;
72
73
74 static void
75 impl_save_to_xml (RBPlaylistSource *source, xmlNodePtr node)
76 {
77 /* do nothing; just to prevent weirdness */
78 }
79
80 static void
81 set_field_from_property (TotemPlPlaylist *playlist,
82 TotemPlPlaylistIter *iter,
83 RhythmDBEntry *entry,
84 RhythmDBPropType property,
85 const char *field)
86 {
87 const char *value;
88
89 value = rhythmdb_entry_get_string (entry, property);
90 if (value != NULL) {
91 totem_pl_playlist_set (playlist, iter, field, value, NULL);
92 }
93 }
94
95 static gboolean
96 save_playlist_foreach (GtkTreeModel *model,
97 GtkTreePath *path,
98 GtkTreeIter *iter,
99 SavePlaylistData *data)
100 {
101 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (data->source);
102 RhythmDBEntry *entry;
103 TotemPlPlaylistIter pl_iter;
104 const char *host_uri;
105 char *uri;
106
107 entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (model), iter);
108 if (entry == NULL) {
109 return FALSE;
110 }
111
112 host_uri = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
113 uri = rb_generic_player_source_uri_to_playlist_uri (priv->player_source, host_uri, data->playlist_type);
114
115 totem_pl_playlist_append (data->playlist, &pl_iter);
116 totem_pl_playlist_set (data->playlist, &pl_iter, TOTEM_PL_PARSER_FIELD_URI, uri, NULL);
117 set_field_from_property (data->playlist, &pl_iter, entry, RHYTHMDB_PROP_ARTIST, TOTEM_PL_PARSER_FIELD_AUTHOR);
118 set_field_from_property (data->playlist, &pl_iter, entry, RHYTHMDB_PROP_GENRE, TOTEM_PL_PARSER_FIELD_GENRE);
119 set_field_from_property (data->playlist, &pl_iter, entry, RHYTHMDB_PROP_ALBUM, TOTEM_PL_PARSER_FIELD_ALBUM);
120 set_field_from_property (data->playlist, &pl_iter, entry, RHYTHMDB_PROP_TITLE, TOTEM_PL_PARSER_FIELD_TITLE);
121 rhythmdb_entry_unref (entry);
122
123 g_free (uri);
124 return FALSE;
125 }
126
127 /* this probably belongs more in totem than here */
128 static const char *
129 playlist_format_extension (TotemPlParserType playlist_type)
130 {
131 switch (playlist_type) {
132 case TOTEM_PL_PARSER_PLS:
133 return ".pls";
134 break;
135 case TOTEM_PL_PARSER_M3U:
136 case TOTEM_PL_PARSER_M3U_DOS:
137 return ".m3u";
138 break;
139 case TOTEM_PL_PARSER_IRIVER_PLA:
140 return ".pla";
141 break;
142 case TOTEM_PL_PARSER_XSPF:
143 return ".xspf";
144 break;
145 default:
146 g_assert_not_reached ();
147 }
148 }
149
150 static gboolean
151 save_playlist (RBGenericPlayerPlaylistSource *source)
152 {
153 TotemPlParser *parser;
154 TotemPlParserType playlist_type;
155 RhythmDBQueryModel *query_model;
156 char *name;
157 char *temp_path;
158 GError *error = NULL;
159 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (source);
160 GFile *file;
161 gboolean result;
162 SavePlaylistData data;
163
164 priv->save_playlist_id = 0;
165 playlist_type = rb_generic_player_source_get_playlist_format (priv->player_source);
166
167 g_object_get (source,
168 "name", &name,
169 "base-query-model", &query_model,
170 NULL);
171
172 /* if we don't already have a name for this playlist, make one now */
173 if (priv->playlist_path == NULL) {
174 char *playlist_dir;
175 char *mount_uri;
176 char *filename;
177 const char *ext;
178 GFile *dir;
179 GFile *playlist;
180
181 ext = playlist_format_extension (playlist_type);
182
183 if (name == NULL || name[0] == '\0') {
184 /* now what? */
185 filename = g_strdup_printf ("unnamed%s", ext);
186 } else {
187 filename = g_strdup_printf ("%s%s", name, ext);
188 }
189
190 playlist_dir = rb_generic_player_source_get_playlist_path (priv->player_source);
191 mount_uri = rb_generic_player_source_get_mount_path (priv->player_source);
192
193 dir = g_file_new_for_uri (mount_uri);
194 if (playlist_dir != NULL) {
195 GFile *pdir;
196
197 pdir = g_file_resolve_relative_path (dir, playlist_dir);
198 g_object_unref (dir);
199 dir = pdir;
200 }
201
202 playlist = g_file_resolve_relative_path (dir, filename);
203 priv->playlist_path = g_file_get_path (playlist);
204
205 g_free (mount_uri);
206 g_free (playlist_dir);
207
208 g_object_unref (dir);
209 }
210
211 temp_path = g_strdup_printf ("%s%06X", priv->playlist_path, g_random_int_range (0, 0xFFFFFF));
212 file = g_file_new_for_path (temp_path);
213
214 parser = totem_pl_parser_new ();
215 data.source = source;
216 data.playlist_type = playlist_type;
217 data.playlist = totem_pl_playlist_new ();
218
219 gtk_tree_model_foreach (GTK_TREE_MODEL (query_model),
220 (GtkTreeModelForeachFunc) save_playlist_foreach,
221 &data);
222 if (rb_debug_matches ("totem_pl_parser_save", "totem-pl-parser.c")) {
223 g_object_set (parser, "debug", TRUE, NULL);
224 }
225
226 result = totem_pl_parser_save (parser, data.playlist, file, name, playlist_type, &error);
227 g_object_unref (data.playlist);
228 data.playlist = NULL;
229
230 if (result == FALSE) {
231 /* XXX report this more usefully */
232 g_warning ("Playlist save failed: %s", error ? error->message : "<no error>");
233 } else {
234 GFile *dest;
235
236 dest = g_file_new_for_path (priv->playlist_path);
237 g_file_move (file, dest, G_FILE_COPY_OVERWRITE | G_FILE_COPY_NO_FALLBACK_FOR_MOVE, NULL, NULL, NULL, &error);
238 if (error != NULL) {
239 /* XXX report this more usefully */
240 g_warning ("moving %s => %s failed: %s", temp_path, priv->playlist_path, error->message);
241 }
242
243 g_object_unref (dest);
244 }
245
246 g_clear_error (&error);
247 g_free (name);
248 g_free (temp_path);
249 g_object_unref (query_model);
250 g_object_unref (parser);
251 g_object_unref (file);
252
253 return FALSE;
254 }
255
256 static void
257 handle_playlist_start_cb (TotemPlParser *playlist,
258 const char *uri,
259 GHashTable *metadata,
260 RBGenericPlayerPlaylistSource *source)
261 {
262 const char *title;
263 title = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_TITLE);
264 if (title != NULL) {
265 rb_debug ("got name '%s' for playlist", title);
266 g_object_set (source, "name", title, NULL);
267 }
268 }
269
270 static void
271 handle_playlist_entry_cb (TotemPlParser *playlist,
272 const char *uri,
273 GHashTable *metadata,
274 RBGenericPlayerPlaylistSource *source)
275 {
276 char *local_uri;
277 char *name;
278 char *canon_uri;
279 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (source);
280
281 local_uri = rb_generic_player_source_uri_from_playlist_uri (priv->player_source, uri);
282 if (local_uri == NULL)
283 return;
284
285 canon_uri = rb_canonicalise_uri (local_uri);
286
287 g_object_get (source, "name", &name, NULL);
288 rb_debug ("adding '%s' as '%s' to playlist '%s' (%s)", uri, canon_uri, name, priv->playlist_path);
289 rb_static_playlist_source_add_location (RB_STATIC_PLAYLIST_SOURCE (source), canon_uri, -1);
290 g_free (canon_uri);
291 g_free (local_uri);
292 g_free (name);
293 }
294
295 static gboolean
296 load_playlist (RBGenericPlayerPlaylistSource *source)
297 {
298 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (source);
299 TotemPlParser *parser;
300 gboolean result;
301 GFile *file;
302 char *name;
303 char *uri;
304
305 if (priv->playlist_path == NULL) {
306 /* this happens when we're creating a new playlist */
307 rb_debug ("playlist has no path; obviously can't load it");
308 g_object_set (source, "name", "", NULL);
309 return TRUE;
310 }
311
312 priv->loading = TRUE;
313 file = g_file_new_for_path (priv->playlist_path);
314
315 /* make a default name for the playlist based on the filename */
316 name = g_file_get_basename (file);
317 g_object_set (source, "name", name, NULL);
318 g_free (name);
319
320 parser = totem_pl_parser_new ();
321 if (rb_debug_matches ("totem_pl_parser_parse", "totem-pl-parser.c")) {
322 g_object_set (parser, "debug", TRUE, NULL);
323 }
324
325 rb_generic_player_source_set_supported_formats (priv->player_source, parser);
326 g_signal_connect (parser,
327 "entry-parsed", G_CALLBACK (handle_playlist_entry_cb),
328 source);
329 g_signal_connect (parser,
330 "playlist-started", G_CALLBACK (handle_playlist_start_cb),
331 source);
332 g_object_set (G_OBJECT (parser), "recurse", FALSE, NULL);
333
334 uri = g_file_get_uri (file);
335 switch (totem_pl_parser_parse_with_base (parser, uri, priv->device_root, FALSE)) {
336 case TOTEM_PL_PARSER_RESULT_SUCCESS:
337 rb_debug ("playlist parsed successfully");
338 result = TRUE;
339 break;
340
341 case TOTEM_PL_PARSER_RESULT_ERROR:
342 rb_debug ("playlist parser returned an error");
343 result = FALSE;
344 break;
345
346 case TOTEM_PL_PARSER_RESULT_UNHANDLED:
347 rb_debug ("playlist parser didn't handle the file");
348 result = FALSE;
349 break;
350
351 case TOTEM_PL_PARSER_RESULT_IGNORED:
352 rb_debug ("playlist parser ignored the file");
353 result = FALSE;
354 break;
355 default:
356 g_assert_not_reached ();
357 }
358 g_free (uri);
359 g_object_unref (file);
360
361 priv->loading = FALSE;
362 return result;
363 }
364
365 static void
366 impl_mark_dirty (RBPlaylistSource *source)
367 {
368 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (source);
369
370 if (priv->loading)
371 return;
372
373 /* save the playlist in a few seconds */
374 if (priv->save_playlist_id != 0) {
375 g_source_remove (priv->save_playlist_id);
376 }
377
378 priv->save_playlist_id = g_timeout_add_seconds (PLAYLIST_SAVE_TIMEOUT,
379 (GSourceFunc) save_playlist,
380 source);
381 }
382
383 RBSource *
384 rb_generic_player_playlist_source_new (RBShell *shell,
385 RBGenericPlayerSource *player_source,
386 const char *playlist_file,
387 const char *device_root,
388 RhythmDBEntryType *entry_type)
389 {
390 RBSource *source;
391 source = RB_SOURCE (g_object_new (RB_TYPE_GENERIC_PLAYER_PLAYLIST_SOURCE,
392 "shell", shell,
393 "is-local", FALSE,
394 "entry-type", entry_type,
395 "player-source", player_source,
396 "playlist-path", playlist_file,
397 "device-root", device_root,
398 NULL));
399
400 if (load_playlist (RB_GENERIC_PLAYER_PLAYLIST_SOURCE (source)) == FALSE) {
401 rb_debug ("playlist didn't parse; killing the source");
402 if (g_object_is_floating (source))
403 g_object_ref_sink (source);
404 g_object_unref (source);
405 return NULL;
406 }
407
408 return source;
409 }
410
411 void
412 rb_generic_player_playlist_delete_from_player (RBGenericPlayerPlaylistSource *source)
413 {
414 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (source);
415
416 if (priv->playlist_path != NULL) {
417 GError *error = NULL;
418 GFile *playlist;
419
420 playlist = g_file_new_for_path (priv->playlist_path);
421 g_file_delete (playlist, NULL, &error);
422 if (error != NULL) {
423 g_warning ("Deleting playlist %s failed: %s", priv->playlist_path, error->message);
424 g_clear_error (&error);
425 }
426 g_object_unref (playlist);
427 } else {
428 rb_debug ("playlist was never saved: nothing to delete");
429 }
430 }
431
432 static void
433 rb_generic_player_playlist_source_init (RBGenericPlayerPlaylistSource *source)
434 {
435 }
436
437 static void
438 impl_dispose (GObject *object)
439 {
440 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (object);
441
442 if (priv->save_playlist_id != 0) {
443 g_source_remove (priv->save_playlist_id);
444 save_playlist (RB_GENERIC_PLAYER_PLAYLIST_SOURCE (object));
445 }
446
447 if (priv->player_source != NULL) {
448 g_object_unref (priv->player_source);
449 priv->player_source = NULL;
450 }
451
452 G_OBJECT_CLASS (rb_generic_player_playlist_source_parent_class)->dispose (object);
453 }
454
455 static void
456 impl_finalize (GObject *object)
457 {
458 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (object);
459
460 g_free (priv->playlist_path);
461
462 G_OBJECT_CLASS (rb_generic_player_playlist_source_parent_class)->finalize (object);
463 }
464
465 static void
466 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
467 {
468 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (object);
469
470 switch (prop_id) {
471 case PROP_PLAYER_SOURCE:
472 g_value_set_object (value, priv->player_source);
473 break;
474 case PROP_PLAYLIST_PATH:
475 g_value_set_string (value, priv->playlist_path);
476 break;
477 case PROP_DEVICE_ROOT:
478 g_value_set_string (value, priv->device_root);
479 break;
480 default:
481 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
482 break;
483 }
484 }
485
486 static void
487 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
488 {
489 RBGenericPlayerPlaylistSourcePrivate *priv = GET_PRIVATE (object);
490
491 switch (prop_id) {
492 case PROP_PLAYER_SOURCE:
493 priv->player_source = RB_GENERIC_PLAYER_SOURCE (g_value_dup_object (value));
494 break;
495 case PROP_PLAYLIST_PATH:
496 priv->playlist_path = g_value_dup_string (value);
497 break;
498 case PROP_DEVICE_ROOT:
499 priv->device_root = g_value_dup_string (value);
500 break;
501 default:
502 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
503 break;
504 }
505 }
506
507 static gboolean
508 impl_show_popup (RBDisplayPage *page)
509 {
510 _rb_display_page_show_popup (page, "/GenericPlayerPlaylistSourcePopup");
511 return TRUE;
512 }
513
514 static void
515 rb_generic_player_playlist_source_class_init (RBGenericPlayerPlaylistSourceClass *klass)
516 {
517 GObjectClass *object_class = G_OBJECT_CLASS (klass);
518 RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
519 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
520 RBPlaylistSourceClass *playlist_class = RB_PLAYLIST_SOURCE_CLASS (klass);
521
522 object_class->dispose = impl_dispose;
523 object_class->finalize = impl_finalize;
524 object_class->get_property = impl_get_property;
525 object_class->set_property = impl_set_property;
526
527 page_class->show_popup = impl_show_popup;
528
529 source_class->impl_can_move_to_trash = (RBSourceFeatureFunc) rb_false_function;
530
531 playlist_class->impl_save_contents_to_xml = impl_save_to_xml;
532 playlist_class->impl_mark_dirty = impl_mark_dirty;
533
534 g_object_class_install_property (object_class,
535 PROP_PLAYER_SOURCE,
536 g_param_spec_object ("player-source",
537 "player-source",
538 "player source",
539 RB_TYPE_GENERIC_PLAYER_SOURCE,
540 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
541 g_object_class_install_property (object_class,
542 PROP_PLAYLIST_PATH,
543 g_param_spec_string ("playlist-path",
544 "playlist-path",
545 "path to playlist file",
546 NULL,
547 G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
548
549 g_object_class_install_property (object_class,
550 PROP_DEVICE_ROOT,
551 g_param_spec_string ("device-root",
552 "device-root",
553 "path to root of the device",
554 NULL,
555 G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
556
557 g_type_class_add_private (klass, sizeof (RBGenericPlayerPlaylistSourcePrivate));
558 }
559
560 static void
561 rb_generic_player_playlist_source_class_finalize (RBGenericPlayerPlaylistSourceClass *klass)
562 {
563 }
564
565 void
566 _rb_generic_player_playlist_source_register_type (GTypeModule *module)
567 {
568 rb_generic_player_playlist_source_register_type (module);
569 }