1 /*******************************************************************************
2 * xspf.c : XSPF playlist import functions
3 *******************************************************************************
4 * Copyright (C) 2006 the VideoLAN team
7 * Authors: Daniel Stränger <vlc at schmaller dot de>
8 * Yoann Peronneau <yoann@videolan.org>
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23 ******************************************************************************/
25 * \file modules/demux/playlist/xspf.c
26 * \brief XSPF playlist import functions
33 #include <vlc_common.h>
34 #include <vlc_demux.h>
37 #include <vlc_strings.h>
44 input_item_t **pp_tracklist;
45 int i_tracklist_entries;
50 static int Control(demux_t *, int, va_list);
51 static int Demux(demux_t *);
54 * \brief XSPF submodule initialization function
56 int Import_xspf(vlc_object_t *p_this)
58 DEMUX_BY_EXTENSION_OR_FORCED_MSG(".xspf", "xspf-open",
59 "using XSPF playlist reader");
63 void Close_xspf(vlc_object_t *p_this)
65 demux_t *p_demux = (demux_t *)p_this;
66 demux_sys_t *p_sys = p_demux->p_sys;
67 for (int i = 0; i < p_sys->i_tracklist_entries; i++)
68 if (p_sys->pp_tracklist[i])
69 vlc_gc_decref(p_sys->pp_tracklist[i]);
70 free(p_sys->pp_tracklist);
71 free(p_sys->psz_base);
76 * \brief demuxer function for XSPF parsing
78 static int Demux(demux_t *p_demux)
81 xml_reader_t *p_xml_reader = NULL;
82 const char *name = NULL;
83 input_item_t *p_current_input = GetCurrentItem(p_demux);
84 p_demux->p_sys->pp_tracklist = NULL;
85 p_demux->p_sys->i_tracklist_entries = 0;
86 p_demux->p_sys->i_track_id = -1;
87 p_demux->p_sys->psz_base = NULL;
89 /* create new xml parser from stream */
90 p_xml_reader = xml_ReaderCreate(p_demux, p_demux->s);
94 /* locating the root node */
95 if (xml_ReaderNextNode(p_xml_reader, &name) != XML_READER_STARTELEM)
97 msg_Err(p_demux, "can't read xml stream");
101 /* checking root node name */
102 if (strcmp(name, "playlist"))
104 msg_Err(p_demux, "invalid root node name <%s>", name);
108 input_item_node_t *p_subitems =
109 input_item_node_Create(p_current_input);
111 i_ret = parse_playlist_node(p_demux, p_subitems,
112 p_xml_reader, "playlist") ? 0 : -1;
114 for (int i = 0 ; i < p_demux->p_sys->i_tracklist_entries ; i++)
116 input_item_t *p_new_input = p_demux->p_sys->pp_tracklist[i];
119 input_item_node_AppendItem(p_subitems, p_new_input);
123 input_item_node_PostAndDelete(p_subitems);
126 vlc_gc_decref(p_current_input);
128 xml_ReaderDelete(p_xml_reader);
129 return i_ret; /* Needed for correct operation of go back */
132 /** \brief dummy function for demux callback interface */
133 static int Control(demux_t *p_demux, int i_query, va_list args)
135 VLC_UNUSED(p_demux); VLC_UNUSED(i_query); VLC_UNUSED(args);
140 * \brief parse the root node of a XSPF playlist
141 * \param p_demux demuxer instance
142 * \param p_input_item current input item
143 * \param p_xml_reader xml reader instance
144 * \param psz_element name of element to parse
146 static bool parse_playlist_node COMPLEX_INTERFACE
148 input_item_t *p_input_item = p_input_node->p_item;
149 char *psz_value = NULL;
150 bool b_version_found = false;
153 xml_elem_hnd_t *p_handler = NULL;
155 xml_elem_hnd_t pl_elements[] =
156 { {"title", SIMPLE_CONTENT, {.smpl = set_item_info} },
157 {"creator", SIMPLE_CONTENT, {.smpl = set_item_info} },
158 {"annotation", SIMPLE_CONTENT, {.smpl = set_item_info} },
159 {"info", SIMPLE_CONTENT, {NULL} },
160 {"location", SIMPLE_CONTENT, {NULL} },
161 {"identifier", SIMPLE_CONTENT, {NULL} },
162 {"image", SIMPLE_CONTENT, {.smpl = set_item_info} },
163 {"date", SIMPLE_CONTENT, {NULL} },
164 {"license", SIMPLE_CONTENT, {NULL} },
165 {"attribution", COMPLEX_CONTENT, {.cmplx = skip_element} },
166 {"link", SIMPLE_CONTENT, {NULL} },
167 {"meta", SIMPLE_CONTENT, {NULL} },
168 {"extension", COMPLEX_CONTENT, {.cmplx = parse_extension_node} },
169 {"trackList", COMPLEX_CONTENT, {.cmplx = parse_tracklist_node} },
170 {NULL, UNKNOWN_CONTENT, {NULL} }
173 /* read all playlist attributes */
174 const char *name, *value;
175 while ((name = xml_ReaderNextAttr(p_xml_reader, &value)) != NULL)
177 /* attribute: version */
178 if (!strcmp(name, "version"))
180 b_version_found = true;
181 if (strcmp(value, "0") && strcmp(value, "1"))
182 msg_Warn(p_demux, "unsupported XSPF version %s", value);
184 /* attribute: xmlns */
185 else if (!strcmp(name, "xmlns") || !strcmp(name, "xmlns:vlc"))
187 else if (!strcmp(name, "xml:base"))
189 free(p_demux->p_sys->psz_base);
190 p_demux->p_sys->psz_base = strdup(psz_value);
192 /* unknown attribute */
194 msg_Warn(p_demux, "invalid <playlist> attribute: \"%s\"", name);
196 /* attribute version is mandatory !!! */
197 if (!b_version_found)
198 msg_Warn(p_demux, "<playlist> requires \"version\" attribute");
200 /* parse the child elements - we only take care of <trackList> */
202 while ((i_node = xml_ReaderNextNode(p_xml_reader, &name)) > 0)
205 /* element start tag */
206 case XML_READER_STARTELEM:
209 msg_Err(p_demux, "invalid XML stream");
213 for (p_handler = pl_elements;
214 p_handler->name && strcmp(name, p_handler->name);
216 if (!p_handler->name)
218 msg_Err(p_demux, "unexpected element <%s>", name);
221 /* complex content is parsed in a separate function */
222 if (p_handler->type == COMPLEX_CONTENT)
225 if (!p_handler->pf_handler.cmplx(p_demux, p_input_node,
226 p_xml_reader, p_handler->name))
232 /* simple element content */
233 case XML_READER_TEXT:
234 psz_value = strdup(name);
239 /* element end tag */
240 case XML_READER_ENDELEM:
241 /* leave if the current parent node <playlist> is terminated */
242 if (!strcmp(name, psz_element))
247 /* there MUST have been a start tag for that element name */
248 if (!p_handler || !p_handler->name || strcmp(p_handler->name, name))
250 msg_Err(p_demux, "there's no open element left for <%s>", name);
254 if (p_handler->pf_handler.smpl)
255 p_handler->pf_handler.smpl(p_input_item, p_handler->name, psz_value);
267 * \brief parses the tracklist node which only may contain <track>s
269 static bool parse_tracklist_node COMPLEX_INTERFACE
271 VLC_UNUSED(psz_element);
273 unsigned i_ntracks = 0;
276 /* now parse the <track>s */
277 while ((i_node = xml_ReaderNextNode(p_xml_reader, &name)) > 0)
279 if (i_node == XML_READER_STARTELEM)
281 if (strcmp(name, "track"))
283 msg_Err(p_demux, "unexpected child of <trackList>: <%s>",
288 /* parse the track data in a separate function */
289 if (parse_track_node(p_demux, p_input_node, p_xml_reader, "track"))
292 else if (i_node == XML_READER_ENDELEM)
296 /* the <trackList> has to be terminated */
297 if (i_node != XML_READER_ENDELEM)
299 msg_Err(p_demux, "there's a missing </trackList>");
302 if (strcmp(name, "trackList"))
304 msg_Err(p_demux, "expected: </trackList>, found: </%s>", name);
308 msg_Dbg(p_demux, "parsed %u tracks successfully", i_ntracks);
313 * \brief parse one track element
314 * \param COMPLEX_INTERFACE
316 static bool parse_track_node COMPLEX_INTERFACE
318 input_item_t *p_input_item = p_input_node->p_item;
320 char *psz_value = NULL;
321 const xml_elem_hnd_t *p_handler = NULL;
322 demux_sys_t *p_sys = p_demux->p_sys;
325 static const xml_elem_hnd_t track_elements[] =
326 { {"location", SIMPLE_CONTENT, {NULL} },
327 {"identifier", SIMPLE_CONTENT, {NULL} },
328 {"title", SIMPLE_CONTENT, {.smpl = set_item_info} },
329 {"creator", SIMPLE_CONTENT, {.smpl = set_item_info} },
330 {"annotation", SIMPLE_CONTENT, {.smpl = set_item_info} },
331 {"info", SIMPLE_CONTENT, {NULL} },
332 {"image", SIMPLE_CONTENT, {.smpl = set_item_info} },
333 {"album", SIMPLE_CONTENT, {.smpl = set_item_info} },
334 {"trackNum", SIMPLE_CONTENT, {.smpl = set_item_info} },
335 {"duration", SIMPLE_CONTENT, {.smpl = set_item_info} },
336 {"link", SIMPLE_CONTENT, {NULL} },
337 {"meta", SIMPLE_CONTENT, {NULL} },
338 {"extension", COMPLEX_CONTENT, {.cmplx = parse_extension_node} },
339 {NULL, UNKNOWN_CONTENT, {NULL} }
342 input_item_t *p_new_input = input_item_New(NULL, NULL);
345 input_item_node_t *p_new_node = input_item_node_Create(p_new_input);
347 /* reset i_track_id */
348 p_sys->i_track_id = -1;
350 while ((i_node = xml_ReaderNextNode(p_xml_reader, &name)) > 0)
353 /* element start tag */
354 case XML_READER_STARTELEM:
357 msg_Err(p_demux, "invalid XML stream");
361 for (p_handler = track_elements;
362 p_handler->name && strcmp(name, p_handler->name);
364 if (!p_handler->name)
366 msg_Err(p_demux, "unexpected element <%s>", name);
369 /* complex content is parsed in a separate function */
370 if (p_handler->type == COMPLEX_CONTENT)
374 if (!p_handler->pf_handler.cmplx(p_demux, p_new_node,
375 p_xml_reader, p_handler->name)) {
376 input_item_node_Delete(p_new_node);
384 /* simple element content */
385 case XML_READER_TEXT:
387 psz_value = strdup(name);
388 if (unlikely(!psz_value))
392 /* element end tag */
393 case XML_READER_ENDELEM:
394 /* leave if the current parent node <track> is terminated */
395 if (!strcmp(name, psz_element))
399 /* Make sure we have a URI */
400 char *psz_uri = input_item_GetURI(p_new_input);
402 input_item_SetURI(p_new_input, "vlc://nop");
406 if (p_sys->i_track_id < 0
407 || p_sys->i_track_id >= (SIZE_MAX / sizeof(p_new_input)))
409 input_item_node_AppendNode(p_input_node, p_new_node);
410 vlc_gc_decref(p_new_input);
414 if (p_sys->i_track_id >= p_sys->i_tracklist_entries)
417 pp = realloc(p_sys->pp_tracklist,
418 (p_sys->i_track_id + 1) * sizeof(*pp));
421 vlc_gc_decref(p_new_input);
422 input_item_node_Delete(p_new_node);
425 p_sys->pp_tracklist = pp;
426 while (p_sys->i_track_id >= p_sys->i_tracklist_entries)
427 pp[p_sys->i_tracklist_entries++] = NULL;
429 else if (p_sys->pp_tracklist[p_sys->i_track_id] != NULL)
431 msg_Err(p_demux, "track ID %d collision", p_sys->i_track_id);
432 vlc_gc_decref(p_new_input);
433 input_item_node_Delete(p_new_node);
437 p_sys->pp_tracklist[ p_sys->i_track_id ] = p_new_input;
438 input_item_node_Delete(p_new_node);
441 /* there MUST have been a start tag for that element name */
442 if (!p_handler || !p_handler->name || strcmp(p_handler->name, name))
444 msg_Err(p_demux, "there's no open element left for <%s>", name);
448 /* special case: location */
449 if (!strcmp(p_handler->name, "location"))
451 if (psz_value == NULL)
452 input_item_SetURI(p_new_input, "vlc://nop");
454 /* FIXME (#4005): This is broken. Scheme-relative (//...) locations
455 * and anchors (#...) are not resolved correctly. Also,
456 * host-relative (/...) and directory-relative locations
457 * ("relative path" in vernacular) should be resolved.
458 * Last, psz_base should default to the XSPF resource
459 * location if missing (not the current working directory).
461 if (p_sys->psz_base && !strstr(psz_value, "://"))
464 if (asprintf(&psz_tmp, "%s%s", p_sys->psz_base, psz_value)
469 input_item_SetURI(p_new_input, psz_tmp);
473 input_item_SetURI(p_new_input, psz_value);
474 input_item_CopyOptions(p_input_item, p_new_input);
478 /* there MUST be an item */
479 if (p_handler->pf_handler.smpl)
481 p_handler->pf_handler.smpl(p_new_input,
491 msg_Err(p_demux, "unexpected end of xml data");
495 input_item_node_Delete(p_new_node);
501 * \brief handles the supported <track> sub-elements
503 static bool set_item_info SIMPLE_INTERFACE
505 /* exit if setting is impossible */
506 if (!psz_name || !psz_value || !p_input)
509 /* re-convert xml special characters inside psz_value */
510 resolve_xml_special_chars(psz_value);
512 /* handle each info element in a separate "if" clause */
513 if (!strcmp(psz_name, "title"))
514 input_item_SetTitle(p_input, psz_value);
515 else if (!strcmp(psz_name, "creator"))
516 input_item_SetArtist(p_input, psz_value);
517 else if (!strcmp(psz_name, "album"))
518 input_item_SetAlbum(p_input, psz_value);
519 else if (!strcmp(psz_name, "trackNum"))
520 input_item_SetTrackNum(p_input, psz_value);
521 else if (!strcmp(psz_name, "duration"))
523 long i_num = atol(psz_value);
524 input_item_SetDuration(p_input, (mtime_t) i_num*1000);
526 else if (!strcmp(psz_name, "annotation"))
527 input_item_SetDescription(p_input, psz_value);
528 else if (!strcmp(psz_name, "image"))
529 input_item_SetArtURL(p_input, psz_value);
534 * \brief handles the <vlc:option> elements
536 static bool set_option SIMPLE_INTERFACE
538 /* exit if setting is impossible */
539 if (!psz_name || !psz_value || !p_input)
542 /* re-convert xml special characters inside psz_value */
543 resolve_xml_special_chars(psz_value);
545 input_item_AddOption(p_input, psz_value, 0);
551 * \brief parse the extension node of a XSPF playlist
553 static bool parse_extension_node COMPLEX_INTERFACE
555 input_item_t *p_input_item = p_input_node->p_item;
556 char *psz_value = NULL;
557 char *psz_title = NULL;
558 char *psz_application = NULL;
560 bool b_release_input_item = false;
561 xml_elem_hnd_t *p_handler = NULL;
562 input_item_t *p_new_input = NULL;
564 xml_elem_hnd_t pl_elements[] =
565 { {"vlc:node", COMPLEX_CONTENT, {.cmplx = parse_extension_node} },
566 {"vlc:item", COMPLEX_CONTENT, {.cmplx = parse_extitem_node} },
567 {"vlc:id", SIMPLE_CONTENT, {NULL} },
568 {"vlc:option", SIMPLE_CONTENT, {.smpl = set_option} },
569 {NULL, UNKNOWN_CONTENT, {NULL} }
572 /* read all extension node attributes */
573 const char *name, *value;
574 while ((name = xml_ReaderNextAttr(p_xml_reader, &value)) != NULL)
576 /* attribute: title */
577 if (!strcmp(name, "title"))
580 psz_title = strdup(value);
581 if (likely(psz_title != NULL))
582 resolve_xml_special_chars(psz_title);
584 /* extension attribute: application */
585 else if (!strcmp(name, "application"))
587 free(psz_application);
588 psz_application = strdup(value);
590 /* unknown attribute */
592 msg_Warn(p_demux, "invalid <%s> attribute:\"%s\"", psz_element,
596 /* attribute title is mandatory except for <extension> */
597 if (!strcmp(psz_element, "vlc:node"))
601 msg_Warn(p_demux, "<vlc:node> requires \"title\" attribute");
604 p_new_input = input_item_NewWithType("vlc://nop", psz_title,
606 ITEM_TYPE_DIRECTORY);
610 input_item_node_AppendItem(p_input_node, p_new_input);
611 p_input_item = p_new_input;
612 b_release_input_item = true;
616 else if (!strcmp(psz_element, "extension"))
618 if (!psz_application)
620 msg_Warn(p_demux, "<extension> requires \"application\" attribute");
623 /* Skip the extension if the application is not vlc
624 This will skip all children of the current node */
625 else if (strcmp(psz_application, "http://www.videolan.org/vlc/playlist/0"))
627 msg_Dbg(p_demux, "Skipping \"%s\" extension tag", psz_application);
628 free(psz_application);
629 /* Skip all children */
630 for (unsigned lvl = 1; lvl;)
631 switch (xml_ReaderNextNode(p_xml_reader, NULL))
633 case XML_READER_STARTELEM: lvl++; break;
634 case XML_READER_ENDELEM: lvl--; break;
635 case 0: case -1: return -1;
640 free(psz_application);
643 /* parse the child elements */
644 while ((i_node = xml_ReaderNextNode(p_xml_reader, &name)) > 0)
648 /* element start tag */
649 case XML_READER_STARTELEM:
652 msg_Err(p_demux, "invalid xml stream");
654 if (b_release_input_item) vlc_gc_decref(p_new_input);
658 for (p_handler = pl_elements;
659 p_handler->name && strcmp(name, p_handler->name);
661 if (!p_handler->name)
663 msg_Err(p_demux, "unexpected element <%s>", name);
665 if (b_release_input_item) vlc_gc_decref(p_new_input);
668 /* complex content is parsed in a separate function */
669 if (p_handler->type == COMPLEX_CONTENT)
671 if (p_handler->pf_handler.cmplx(p_demux,
682 if (b_release_input_item) vlc_gc_decref(p_new_input);
688 case XML_READER_TEXT:
689 /* simple element content */
691 psz_value = strdup(name);
692 if (unlikely(!psz_value))
695 if (b_release_input_item) vlc_gc_decref(p_new_input);
700 /* element end tag */
701 case XML_READER_ENDELEM:
702 /* leave if the current parent node is terminated */
703 if (!strcmp(name, psz_element))
706 if (b_release_input_item) vlc_gc_decref(p_new_input);
709 /* there MUST have been a start tag for that element name */
710 if (!p_handler || !p_handler->name
711 || strcmp(p_handler->name, name))
713 msg_Err(p_demux, "there's no open element left for <%s>",
716 if (b_release_input_item) vlc_gc_decref(p_new_input);
720 /* special tag <vlc:id> */
721 if (!strcmp(p_handler->name, "vlc:id"))
723 p_demux->p_sys->i_track_id = atoi(psz_value);
725 else if (p_handler->pf_handler.smpl)
727 p_handler->pf_handler.smpl(p_input_item, p_handler->name,
735 if (b_release_input_item) vlc_gc_decref(p_new_input);
740 * \brief parse the extension item node of a XSPF playlist
742 static bool parse_extitem_node COMPLEX_INTERFACE
744 VLC_UNUSED(psz_element);
745 input_item_t *p_new_input = NULL;
748 /* read all extension item attributes */
749 const char *name, *value;
750 while ((name = xml_ReaderNextAttr(p_xml_reader, &value)) != NULL)
752 /* attribute: href */
753 if (!strcmp(name, "tid"))
755 /* unknown attribute */
757 msg_Warn(p_demux, "invalid <vlc:item> attribute: \"%s\"", name);
760 /* attribute href is mandatory */
763 msg_Warn(p_demux, "<vlc:item> requires \"tid\" attribute");
767 if (i_tid >= p_demux->p_sys->i_tracklist_entries)
769 msg_Warn(p_demux, "invalid \"tid\" attribute");
773 p_new_input = p_demux->p_sys->pp_tracklist[ i_tid ];
776 input_item_node_AppendItem(p_input_node, p_new_input);
777 vlc_gc_decref(p_new_input);
778 p_demux->p_sys->pp_tracklist[i_tid] = NULL;
785 * \brief skips complex element content that we can't manage
787 static bool skip_element COMPLEX_INTERFACE
789 VLC_UNUSED(p_demux); VLC_UNUSED(p_input_node);
790 VLC_UNUSED(psz_element);
792 for (unsigned lvl = 1; lvl;)
793 switch (xml_ReaderNextNode(p_xml_reader, NULL))
795 case XML_READER_STARTELEM: lvl++; break;
796 case XML_READER_ENDELEM: lvl--; break;
797 case 0: case -1: return false;