1 /*
2 Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Bastien Nocera
3 Copyright (C) 2003, 2004 Colin Walters <walters@rhythmbox.org>
4
5 The Gnome Library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public License as
7 published by the Free Software Foundation; either version 2 of the
8 License, or (at your option) any later version.
9
10 The Gnome Library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public
16 License along with the Gnome Library; see the file COPYING.LIB. If not,
17 write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301 USA.
19
20 Author: Bastien Nocera <hadess@hadess.net>
21 */
22
23 #include "config.h"
24
25 #include <string.h>
26 #include <stdlib.h>
27 #include <glib.h>
28
29 #ifndef TOTEM_PL_PARSER_MINI
30 #include <glib/gi18n-lib.h>
31
32 #include <gio/gio.h>
33
34 #include "totem-pl-parser.h"
35 #include "totem-pl-parser-pls.h"
36 #endif /* !TOTEM_PL_PARSER_MINI */
37
38 #include "totem-pl-parser-mini.h"
39 #include "totem-pl-parser-lines.h"
40 #include "totem-pl-parser-private.h"
41
42 #ifndef TOTEM_PL_PARSER_MINI
43
44 #define EXTINF "#EXTINF:"
45 #define EXTINF_HLS "#EXT-X-STREAM-INF"
46 #define EXTINF_HLS2 "#EXT-X-TARGETDURATION"
47 #define EXTVLCOPT_AUDIOTRACK "#EXTVLCOPT:audio-track-id="
48
49 static char *
totem_pl_parser_uri_to_dos(const char * uri,GFile * output)50 totem_pl_parser_uri_to_dos (const char *uri, GFile *output)
51 {
52 char *retval, *i;
53
54 /* Get a relative URI if there is one */
55 retval = totem_pl_parser_relative (output, uri);
56
57 if (retval == NULL)
58 retval = g_strdup (uri);
59
60 /* Don't change URIs, but change smb:// */
61 if (g_str_has_prefix (retval, "smb://") != FALSE) {
62 char *tmp;
63 tmp = g_strdup (retval + strlen ("smb:"));
64 g_free (retval);
65 retval = tmp;
66 }
67
68 if (strstr (retval, "://") != NULL)
69 return retval;
70
71 i = retval;
72 while (*i != '\0')
73 {
74 if (*i == '/')
75 *i = '\\';
76 i++;
77 }
78
79 return retval;
80 }
81
82 gboolean
totem_pl_parser_save_m3u(TotemPlParser * parser,TotemPlPlaylist * playlist,GFile * output,gboolean dos_compatible,GCancellable * cancellable,GError ** error)83 totem_pl_parser_save_m3u (TotemPlParser *parser,
84 TotemPlPlaylist *playlist,
85 GFile *output,
86 gboolean dos_compatible,
87 GCancellable *cancellable,
88 GError **error)
89 {
90 TotemPlPlaylistIter iter;
91 GFileOutputStream *stream;
92 gboolean valid, success;
93 char *buf;
94 const char *cr;
95
96 stream = g_file_replace (output, NULL, FALSE, G_FILE_CREATE_NONE, cancellable, error);
97 if (stream == NULL)
98 return FALSE;
99
100 cr = dos_compatible ? "\r\n" : "\n";
101
102 buf = g_strdup_printf ("#EXTM3U%s", cr);
103 success = totem_pl_parser_write_string (G_OUTPUT_STREAM (stream), buf, cancellable, error);
104 g_free (buf);
105 if (success == FALSE)
106 return FALSE;
107
108 valid = totem_pl_playlist_iter_first (playlist, &iter);
109
110 while (valid) {
111 char *uri, *title, *path2;
112 GFile *file;
113
114 totem_pl_playlist_get (playlist, &iter,
115 TOTEM_PL_PARSER_FIELD_URI, &uri,
116 TOTEM_PL_PARSER_FIELD_TITLE, &title,
117 NULL);
118
119 valid = totem_pl_playlist_iter_next (playlist, &iter);
120
121 if (!uri) {
122 g_free (title);
123 continue;
124 }
125
126 file = g_file_new_for_uri (uri);
127
128 if (totem_pl_parser_scheme_is_ignored (parser, file) != FALSE) {
129 g_object_unref (file);
130 g_free (uri);
131 g_free (title);
132 continue;
133 }
134 g_object_unref (file);
135
136 if (title) {
137 buf = g_strdup_printf (EXTINF",%s%s", title, cr);
138 success = totem_pl_parser_write_string (G_OUTPUT_STREAM (stream), buf, cancellable, error);
139 g_free (buf);
140 if (success == FALSE) {
141 g_free (title);
142 g_free (uri);
143 return FALSE;
144 }
145 }
146 g_free (title);
147
148 if (dos_compatible == FALSE) {
149 char *tmp;
150
151 tmp = totem_pl_parser_relative (output, uri);
152
153 if (tmp == NULL && g_str_has_prefix (uri, "file:")) {
154 path2 = g_filename_from_uri (uri, NULL, NULL);
155 } else {
156 path2 = tmp;
157 }
158 } else {
159 path2 = totem_pl_parser_uri_to_dos (uri, output);
160 }
161
162 buf = g_strdup_printf ("%s%s", path2 ? path2 : uri, cr);
163 g_free (path2);
164 g_free (uri);
165
166 success = totem_pl_parser_write_string (G_OUTPUT_STREAM (stream), buf, cancellable, error);
167 g_free (buf);
168
169 if (success == FALSE)
170 return FALSE;
171 }
172
173 g_object_unref (stream);
174
175 return TRUE;
176 }
177
178 static void
totem_pl_parser_parse_ram_uri(TotemPlParser * parser,const char * uri)179 totem_pl_parser_parse_ram_uri (TotemPlParser *parser, const char *uri)
180 {
181 char *mark, **params;
182 GString *str;
183 guint i, num_params;
184 char *title, *author, *copyright, *abstract, *screensize, *mode, *start, *end;
185
186 if (g_str_has_prefix (uri, "rtsp://") == FALSE
187 && g_str_has_prefix (uri, "pnm://") == FALSE) {
188 totem_pl_parser_add_one_uri (parser, uri, NULL);
189 return;
190 }
191
192 /* Look for "?" */
193 mark = strstr (uri, "?");
194 if (mark == NULL) {
195 totem_pl_parser_add_one_uri (parser, uri, NULL);
196 return;
197 }
198
199 if (mark[1] == '\0') {
200 char *new_uri;
201
202 new_uri = g_strndup (uri, mark + 1 - uri);
203 totem_pl_parser_add_one_uri (parser, new_uri, NULL);
204 g_free (new_uri);
205 return;
206 }
207
208 title = author = copyright = abstract = screensize = mode = end = start = NULL;
209 num_params = 0;
210
211 str = g_string_new_len (uri, mark - uri);
212 params = g_strsplit (mark + 1, "&", -1);
213 for (i = 0; params[i] != NULL; i++) {
214 if (g_str_has_prefix (params[i], "title=") != FALSE) {
215 title = params[i] + strlen ("title=");
216 } else if (g_str_has_prefix (params[i], "author=") != FALSE) {
217 author = params[i] + strlen ("author=");
218 } else if (g_str_has_prefix (params[i], "copyright=") != FALSE) {
219 copyright = params[i] + strlen ("copyright=");
220 } else if (g_str_has_prefix (params[i], "abstract=") != FALSE) {
221 abstract = params[i] + strlen ("abstract=");
222 } else if (g_str_has_prefix (params[i], "screensize=") != FALSE) {
223 screensize = params[i] + strlen ("screensize=");
224 } else if (g_str_has_prefix (params[i], "mode=") != FALSE) {
225 mode = params[i] + strlen ("mode=");
226 } else if (g_str_has_prefix (params[i], "end=") != FALSE) {
227 end = params[i] + strlen ("end=");
228 } else if (g_str_has_prefix (params[i], "start=") != FALSE) {
229 start = params[i] + strlen ("start=");
230 } else {
231 if (num_params == 0)
232 g_string_append_c (str, '?');
233 else
234 g_string_append_c (str, '&');
235 g_string_append (str, params[i]);
236 num_params++;
237 }
238 }
239
240 totem_pl_parser_add_uri (parser,
241 TOTEM_PL_PARSER_FIELD_URI, str->str,
242 TOTEM_PL_PARSER_FIELD_TITLE, title,
243 TOTEM_PL_PARSER_FIELD_AUTHOR, author,
244 TOTEM_PL_PARSER_FIELD_COPYRIGHT, copyright,
245 TOTEM_PL_PARSER_FIELD_ABSTRACT, abstract,
246 TOTEM_PL_PARSER_FIELD_SCREENSIZE, screensize,
247 TOTEM_PL_PARSER_FIELD_UI_MODE, mode,
248 TOTEM_PL_PARSER_FIELD_STARTTIME, start,
249 TOTEM_PL_PARSER_FIELD_ENDTIME, end,
250 NULL);
251
252 g_string_free (str, TRUE);
253 g_strfreev (params);
254 }
255
256 TotemPlParserResult
totem_pl_parser_add_ram(TotemPlParser * parser,GFile * file,TotemPlParseData * parse_data,gpointer data)257 totem_pl_parser_add_ram (TotemPlParser *parser, GFile *file, TotemPlParseData *parse_data, gpointer data)
258 {
259 gboolean retval = TOTEM_PL_PARSER_RESULT_UNHANDLED;
260 char *contents, **lines;
261 gsize size;
262 guint i;
263
264 if (g_file_load_contents (file, NULL, &contents, &size, NULL, NULL) == FALSE)
265 return TOTEM_PL_PARSER_RESULT_ERROR;
266
267 lines = g_strsplit_set (contents, "\r\n", 0);
268 g_free (contents);
269
270 for (i = 0; lines[i] != NULL; i++) {
271 /* Empty line */
272 if (totem_pl_parser_line_is_empty (lines[i]) != FALSE)
273 continue;
274
275 retval = TOTEM_PL_PARSER_RESULT_SUCCESS;
276
277 /* Either it's a URI, or it has a proper path ... */
278 if (strstr(lines[i], "://") != NULL
279 || lines[i][0] == G_DIR_SEPARATOR) {
280 GFile *line_file;
281
282 line_file = g_file_new_for_uri (lines[i]);
283 /* .ram files can contain .smil entries */
284 if (totem_pl_parser_parse_internal (parser, line_file, NULL, parse_data) != TOTEM_PL_PARSER_RESULT_SUCCESS)
285 totem_pl_parser_parse_ram_uri (parser, lines[i]);
286 g_object_unref (line_file);
287 } else if (strcmp (lines[i], "--stop--") == 0) {
288 /* For Real Media playlists, handle the stop command */
289 break;
290 } else {
291 //FIXME
292 #if 0
293 char *base;
294
295 /* Try with a base */
296 base = totem_pl_parser_base_uri (uri);
297
298 if (totem_pl_parser_parse_internal (parser, lines[i], base) != TOTEM_PL_PARSER_RESULT_SUCCESS)
299 {
300 char *fullpath;
301 fullpath = g_strdup_printf ("%s/%s", base, lines[i]);
302 totem_pl_parser_parse_ram_uri (parser, fullpath);
303 g_free (fullpath);
304 }
305 g_free (base);
306 #endif
307 }
308 }
309
310 g_strfreev (lines);
311
312 return retval;
313 }
314
315 static const char *
totem_pl_parser_get_extinfo_title(const char * extinfo)316 totem_pl_parser_get_extinfo_title (const char *extinfo)
317 {
318 const char *res, *sep;
319
320 if (extinfo == NULL)
321 return NULL;
322
323 /* It's bound to have an EXTINF if we have extinfo */
324 res = extinfo + strlen(EXTINF);
325 if (res[0] == '\0')
326 return NULL;
327
328 /* Handle ',' as a field separator */
329 sep = strstr (res, ",");
330 if (sep == NULL || sep[1] == '\0') {
331 if (res[1] == '\0')
332 return NULL;
333 return res;
334 }
335
336 sep++;
337 return sep;
338 }
339
340 static char *
totem_pl_parser_get_extinfo_length(const char * extinfo)341 totem_pl_parser_get_extinfo_length (const char *extinfo)
342 {
343 char *res, **items;
344
345 if (extinfo == NULL)
346 return NULL;
347
348 /* It's bound to have an EXTINF if we have extinfo */
349 res = (char *) extinfo + strlen(EXTINF);
350 if (res[0] == '\0')
351 return NULL;
352
353 /* Handle ',' as a field separator */
354 items = g_strsplit (res, ",", 2);
355 if (!items || !items[0] || *items[0] == '\0') {
356 g_strfreev (items);
357 return NULL;
358 }
359 res = g_strdup (items[0]);
360 g_strfreev (items);
361 return res;
362 }
363
364 static char *
totem_pl_parser_get_extvlcopt_audio_track(const char * line)365 totem_pl_parser_get_extvlcopt_audio_track (const char *line)
366 {
367 int id;
368 char *end;
369
370 if (line == NULL)
371 return NULL;
372 id = strtol (line + strlen (EXTVLCOPT_AUDIOTRACK), &end, 10);
373 if (*end != '\0')
374 return NULL;
375 /* Bizarre VLC quirk? */
376 if (id > 1000)
377 id = id - 1000;
378 return g_strdup_printf ("%d", id);
379 }
380
381 TotemPlParserResult
totem_pl_parser_add_m3u(TotemPlParser * parser,GFile * file,GFile * base_file,TotemPlParseData * parse_data,gpointer data)382 totem_pl_parser_add_m3u (TotemPlParser *parser,
383 GFile *file,
384 GFile *base_file,
385 TotemPlParseData *parse_data,
386 gpointer data)
387 {
388 TotemPlParserResult retval = TOTEM_PL_PARSER_RESULT_UNHANDLED;
389 char *contents, **lines;
390 gsize size;
391 guint i, num_lines;
392 gboolean dos_mode = FALSE;
393 const char *extinfo, *extvlcopt_audiotrack;
394 char *pl_uri;
395
396 if (g_file_load_contents (file, NULL, &contents, &size, NULL, NULL) == FALSE) {
397 DEBUG (file, g_print ("Failed to load '%s'\n", uri));
398 return TOTEM_PL_PARSER_RESULT_ERROR;
399 }
400
401 /* .pls files with a .m3u extension, the nasties */
402 if (g_str_has_prefix (contents, "[playlist]") != FALSE
403 || g_str_has_prefix (contents, "[Playlist]") != FALSE
404 || g_str_has_prefix (contents, "[PLAYLIST]") != FALSE) {
405 DEBUG (file, g_print ("Parsing '%s' playlist as PLS\n", uri));
406 retval = totem_pl_parser_add_pls_with_contents (parser, file, base_file, contents, parse_data);
407 g_free (contents);
408 return retval;
409 }
410
411 if (strstr (contents, EXTINF_HLS) ||
412 strstr (contents, EXTINF_HLS2)) {
413 DEBUG (file, g_print ("Unhandled HLS playlist '%s', should be passed to player\n", uri));
414 g_free (contents);
415 return retval;
416 }
417
418 /* Try to use ISO-8859-1 if we don't have valid UTF-8,
419 * try to parse anyway if it's not ISO-8859-1 */
420 if (g_utf8_validate (contents, -1, NULL) == FALSE) {
421 char *fixed;
422 fixed = g_convert (contents, -1, "UTF-8", "ISO8859-1", NULL, NULL, NULL);
423 if (fixed != NULL) {
424 g_free (contents);
425 contents = fixed;
426 }
427 }
428
429 /* is non-NULL if there's an EXTINF on a preceding line */
430 extinfo = NULL;
431 extvlcopt_audiotrack = NULL;
432
433 /* figure out whether we're a unix m3u or dos m3u */
434 if (strstr(contents, "\x0d")) {
435 dos_mode = TRUE;
436 }
437
438 lines = g_strsplit_set (contents, "\r\n", 0);
439 g_free (contents);
440 num_lines = g_strv_length (lines);
441 /* We don't count the terminating NULL */
442 num_lines--;
443
444 /* Send out the playlist start and get crackin' */
445 pl_uri = g_file_get_uri (file);
446 totem_pl_parser_add_uri (parser,
447 TOTEM_PL_PARSER_FIELD_IS_PLAYLIST, TRUE,
448 TOTEM_PL_PARSER_FIELD_URI, pl_uri,
449 TOTEM_PL_PARSER_FIELD_CONTENT_TYPE, "audio/x-mpegurl",
450 NULL);
451
452 for (i = 0; lines[i] != NULL; i++) {
453 const char *line;
454 char *length;
455 gint64 length_num = 0;
456 char *audio_track;
457
458 line = lines[i];
459
460 if (line[0] == '\0')
461 continue;
462
463 retval = TOTEM_PL_PARSER_RESULT_SUCCESS;
464
465 /* Ignore leading spaces */
466 for (; g_ascii_isspace (line[0]); line++)
467 ;
468
469 /* Ignore comments, but mark it if we have extra info */
470 if (line[0] == '#') {
471 if (extinfo == NULL && g_str_has_prefix (line, EXTINF) != FALSE)
472 extinfo = line;
473 if (extvlcopt_audiotrack == NULL && g_str_has_prefix (line, EXTVLCOPT_AUDIOTRACK) != FALSE)
474 extvlcopt_audiotrack = line;
475 continue;
476 }
477
478 length = totem_pl_parser_get_extinfo_length (extinfo);
479 if (length != NULL)
480 length_num = totem_pl_parser_parse_duration (length, totem_pl_parser_is_debugging_enabled (parser));
481 g_free (length);
482
483 audio_track = totem_pl_parser_get_extvlcopt_audio_track (extvlcopt_audiotrack);
484
485 /* Either it's a URI, or it has a proper path ... */
486 if (strstr(line, "://") != NULL
487 || line[0] == G_DIR_SEPARATOR) {
488 GFile *uri;
489
490 uri = g_file_new_for_commandline_arg (line);
491 if (length_num < 0 ||
492 totem_pl_parser_parse_internal (parser, uri, NULL, parse_data) != TOTEM_PL_PARSER_RESULT_SUCCESS) {
493 totem_pl_parser_add_uri (parser,
494 TOTEM_PL_PARSER_FIELD_URI, line,
495 TOTEM_PL_PARSER_FIELD_TITLE, totem_pl_parser_get_extinfo_title (extinfo),
496 TOTEM_PL_PARSER_FIELD_AUDIO_TRACK, audio_track,
497 NULL);
498 }
499 g_object_unref (uri);
500 } else if (g_ascii_isalpha (line[0]) != FALSE
501 && g_str_has_prefix (line + 1, ":\\")) {
502 /* Path relative to a drive on Windows, we need to use
503 * the base that was passed to us */
504 GFile *uri;
505
506 lines[i] = g_strdelimit (lines[i], "\\", '/');
507 /* + 2, skip drive letter */
508 uri = g_file_get_child (base_file, line + 2);
509 totem_pl_parser_add_uri (parser,
510 TOTEM_PL_PARSER_FIELD_FILE, uri,
511 TOTEM_PL_PARSER_FIELD_TITLE, totem_pl_parser_get_extinfo_title (extinfo),
512 TOTEM_PL_PARSER_FIELD_AUDIO_TRACK, audio_track,
513 NULL);
514 g_object_unref (uri);
515 } else if (line[0] == '\\' && line[1] == '\\') {
516 /* ... Or it's in the windows smb form
517 * (\\machine\share\filename), Note drive names
518 * (C:\ D:\ etc) are unhandled (unknown base for
519 * drive letters) */
520 char *tmpuri;
521
522 lines[i] = g_strdelimit (lines[i], "\\", '/');
523 tmpuri = g_strjoin (NULL, "smb:", line, NULL);
524
525 totem_pl_parser_add_uri (parser,
526 TOTEM_PL_PARSER_FIELD_URI, tmpuri,
527 TOTEM_PL_PARSER_FIELD_TITLE, totem_pl_parser_get_extinfo_title (extinfo),
528 TOTEM_PL_PARSER_FIELD_AUDIO_TRACK, audio_track,
529 NULL);
530
531 g_free (tmpuri);
532 } else {
533 /* Try with a base */
534 GFile *uri, *_base_file;
535 char sep;
536
537 _base_file = g_file_get_parent (file);
538 sep = (dos_mode ? '\\' : '/');
539 if (sep == '\\')
540 lines[i] = g_strdelimit (lines[i], "\\", '/');
541 uri = g_file_get_child (_base_file, line);
542 g_object_unref (_base_file);
543 totem_pl_parser_add_uri (parser,
544 TOTEM_PL_PARSER_FIELD_FILE, uri,
545 TOTEM_PL_PARSER_FIELD_TITLE, totem_pl_parser_get_extinfo_title (extinfo),
546 TOTEM_PL_PARSER_FIELD_AUDIO_TRACK, audio_track,
547 NULL);
548 g_object_unref (uri);
549 }
550 extinfo = NULL;
551 extvlcopt_audiotrack = NULL;
552
553 g_free (audio_track);
554 }
555
556 g_strfreev (lines);
557
558 totem_pl_parser_playlist_end (parser, pl_uri);
559 g_free (pl_uri);
560
561 return retval;
562 }
563
564 TotemPlParserResult
totem_pl_parser_add_m4u(TotemPlParser * parser,GFile * file,GFile * base_file,TotemPlParseData * parse_data,gpointer data)565 totem_pl_parser_add_m4u (TotemPlParser *parser,
566 GFile *file,
567 GFile *base_file,
568 TotemPlParseData *parse_data,
569 gpointer data)
570 {
571 return totem_pl_parser_add_m3u (parser, file,
572 base_file, parse_data, data);
573 }
574
575 TotemPlParserResult
totem_pl_parser_add_ra(TotemPlParser * parser,GFile * file,GFile * base_file,TotemPlParseData * parse_data,gpointer data)576 totem_pl_parser_add_ra (TotemPlParser *parser,
577 GFile *file,
578 GFile *base_file,
579 TotemPlParseData *parse_data,
580 gpointer data)
581 {
582 if (data == NULL || totem_pl_parser_is_uri_list (data, strlen (data)) == NULL) {
583 totem_pl_parser_add_one_file (parser, file, NULL);
584 return TOTEM_PL_PARSER_RESULT_SUCCESS;
585 }
586
587 return totem_pl_parser_add_ram (parser, file, parse_data, NULL);
588 }
589
590 #endif /* !TOTEM_PL_PARSER_MINI */
591
592 #define CHECK_LEN if (i >= len) { return NULL; }
593
594 const char *
totem_pl_parser_is_uri_list(const char * data,gsize len)595 totem_pl_parser_is_uri_list (const char *data, gsize len)
596 {
597 guint i = 0;
598
599 /* Find the first bits of text */
600 while (data[i] == '\n' || data[i] == '\t' || data[i] == ' ') {
601 i++;
602 CHECK_LEN;
603 }
604 CHECK_LEN;
605
606 /* scheme always starts with a letter */
607 if (g_ascii_isalpha (data[i]) == FALSE)
608 return FALSE;
609 while (g_ascii_isalnum (data[i]) != FALSE) {
610 i++;
611 CHECK_LEN;
612 }
613
614 CHECK_LEN;
615
616 /* First non-alphanum character should be a ':' */
617 if (data[i] != ':')
618 return FALSE;
619 i++;
620 CHECK_LEN;
621
622 if (data[i] != '/')
623 return NULL;
624 i++;
625 CHECK_LEN;
626
627 if (data[i] != '/')
628 return NULL;
629
630 return TEXT_URI_TYPE;
631 }
632
633