XSPF support
The current playlist save format (m3u) does not allow to save all information of the paylist tracks, especially the tree structure.
A quick comparison of various playlist formats
XPSF seems a good format, quite simple though powerful through the use of tags.
In my dreams, the saved playlist format of VLC would remember ALL properties set by user for each track, ie all menu options, volume, image propertiers, stream output, etc, and of course the tracks order and the tree structure. And it would be the default autosave format.
Note: xsl stylesheets exist to convert xspf to m3u/html/smil.
Activity
-
Newest first Oldest first
-
Show all activity Show comments only Show history only
- Migration Bot added Component::Core: Playlist Severity::normal Type::feature + 1 deleted label
added Component::Core: Playlist Severity::normal Type::feature + 1 deleted label
- Migration Bot changed milestone to %Features paradize
changed milestone to %Features paradize
patches submitted by Daniel Stränger
Index: src/playlist/loadsave.c =================================================================== --- src/playlist/loadsave.c (Revision 14333) +++ src/playlist/loadsave.c (Arbeitskopie) @@ -127,6 +127,9 @@ msg_Err( p_playlist, "out of memory"); return VLC_ENOMEM; } + p_export->psz_filename = NULL; + if ( psz_filename ) + p_export->psz_filename = strdup( psz_filename ); p_export->p_file = utf8_fopen( psz_filename, "wt" ); if( !p_export->p_file ) { @@ -149,8 +152,12 @@ } module_Unneed( p_playlist , p_module ); + /* Clean up */ fclose( p_export->p_file ); - + if ( p_export->psz_filename ) + free( p_export->psz_filename ); + free ( p_export ); + p_playlist->p_private = NULL; vlc_mutex_unlock( &p_playlist->object_lock ); return VLC_SUCCESS; Index: modules/demux/playlist/xspf.c =================================================================== --- modules/demux/playlist/xspf.c (Revision 0) +++ modules/demux/playlist/xspf.c (Revision 0) @@ -0,0 +1,748 @@ +/****************************************************************************** + * Copyright (C) 2006 Daniel Stränger <vlc at schmaller dot de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *******************************************************************************/ +/** + * \file modules/demux/playlist/xspf.c + * \brief XSPF playlist import functions + */ + +#include <vlc/vlc.h> +#include <vlc/input.h> +#include <vlc/intf.h> + +#include "playlist.h" +#include "vlc_xml.h" +#include "xspf.h" + +/** + * \brief XSPF submodule initialization function + */ +int E_(xspf_import_Activate)( vlc_object_t *p_this ) +{ + demux_t *p_demux = (demux_t *)p_this; + char *psz_ext; + + psz_ext = strrchr ( p_demux->psz_path, '.' ); + + if( ( psz_ext && !strcasecmp( psz_ext, ".xspf") ) || + ( p_demux->psz_demux && !strcmp(p_demux->psz_demux, "xspf-open") ) ) + { + ; + } + else + { + return VLC_EGENERIC; + } + msg_Dbg( p_demux, "using xspf playlist import"); + + p_demux->pf_control = xspf_import_Control; + p_demux->pf_demux = xspf_import_Demux; + + return VLC_SUCCESS; +} + +/** + * \brief demuxer function for XSPF parsing + */ +int xspf_import_Demux( demux_t *p_demux ) +{ + playlist_t *p_playlist = NULL; + playlist_item_t *p_current = NULL; + + vlc_bool_t b_play; + int i_ret = VLC_SUCCESS; + + xml_t *p_xml = NULL; + xml_reader_t *p_xml_reader = NULL; + char *psz_name = NULL; + + /* create new xml parser from stream */ + p_xml = xml_Create( p_demux ); + if( !p_xml ) + i_ret = VLC_ENOMOD; + else + { + p_xml_reader = xml_ReaderCreate( p_xml, p_demux->s ); + if( !p_xml_reader ) + i_ret = VLC_EGENERIC; + } + + /* start with parsing the root node */ + if ( i_ret == VLC_SUCCESS ) + if ( xml_ReaderRead( p_xml_reader ) != 1 ) + { + msg_Err( p_demux, "can't read xml stream" ); + i_ret = VLC_EGENERIC; + } + /* checking root nody type */ + if ( i_ret == VLC_SUCCESS ) + if( xml_ReaderNodeType( p_xml_reader ) != XML_READER_STARTELEM ) + { + msg_Err( p_demux, "invalid root node type: %i", xml_ReaderNodeType( p_xml_reader ) ); + i_ret = VLC_EGENERIC; + } + /* checking root node name */ + if ( i_ret == VLC_SUCCESS ) + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name || strcmp( psz_name, "playlist" ) ) + { + msg_Err( p_demux, "invalid root node name: %s", psz_name ); + i_ret = VLC_EGENERIC; + } + FREE_NAME(); + + /* get the playlist ... */ + if ( i_ret == VLC_SUCCESS ) + { + p_playlist = (playlist_t *) vlc_object_find( p_demux, VLC_OBJECT_PLAYLIST, FIND_PARENT ); + if( !p_playlist ) + { + msg_Err( p_demux, "can't find playlist" ); + i_ret = VLC_ENOOBJ; + } + } + /* ... and its current item (to convert it to a node) */ + if ( i_ret == VLC_SUCCESS ) + { + b_play = E_(FindItem)( p_demux, p_playlist, &p_current ); + playlist_ItemToNode( p_playlist, p_current ); + p_current->input.i_type = ITEM_TYPE_PLAYLIST; + /* parse the playlist node */ + i_ret = parse_playlist_node( p_demux, p_playlist, p_current, p_xml_reader, "playlist" ); + /* true/false - success/egeneric mapping */ + i_ret = ( i_ret==VLC_TRUE ? VLC_SUCCESS : VLC_EGENERIC ); + } + + /* Go back and play the playlist */ + if ( i_ret == VLC_SUCCESS ) + if( b_play && p_playlist->status.p_item && + p_playlist->status.p_item->i_children > 0 ) + { + playlist_Control( p_playlist, PLAYLIST_PLAY ); + } + + if ( p_playlist ) + vlc_object_release( p_playlist ); + if ( p_xml_reader ) + xml_ReaderDelete( p_xml, p_xml_reader ); + if ( p_xml ) + xml_Delete( p_xml ); + + return i_ret; +} + +/** \brief dummy function for demux callback interface */ +int xspf_import_Control( demux_t *p_demux, int i_query, va_list args ) +{ + return VLC_EGENERIC; +} + +/** + * \brief parse the root node of a XSPF playlist + * \param p_demux demuxer instance + * \param p_playlist playlist instance + * \param p_item current playlist node + * \param p_xml_reader xml reader instance + * \param psz_element name of element to parse + */ +static vlc_bool_t parse_playlist_node COMPLEX_INTERFACE +{ + char *psz_name=NULL; + char *psz_value=NULL; + vlc_bool_t b_version_found = VLC_FALSE; + int i_node; + xml_elem_hnd_t *p_handler=NULL; + + xml_elem_hnd_t pl_elements[] = + { {"title", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"creator", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"annotation", SIMPLE_CONTENT, {NULL} }, + {"info", SIMPLE_CONTENT, {NULL} }, + {"location", SIMPLE_CONTENT, {NULL} }, + {"identifier", SIMPLE_CONTENT, {NULL} }, + {"image", SIMPLE_CONTENT, {NULL} }, + {"date", SIMPLE_CONTENT, {NULL} }, + {"license", SIMPLE_CONTENT, {NULL} }, + {"attribution", COMPLEX_CONTENT, {.cmplx = skip_element} }, + {"link", SIMPLE_CONTENT, {NULL} }, + {"meta", SIMPLE_CONTENT, {NULL} }, + {"extension", COMPLEX_CONTENT, {.cmplx = skip_element} }, + {"trackList", COMPLEX_CONTENT, {.cmplx = parse_tracklist_node} }, + {NULL, UNKNOWN_CONTENT, {NULL} } + }; + + /* read all playlist attributes */ + while ( xml_ReaderNextAttr( p_xml_reader ) == VLC_SUCCESS ) + { + psz_name = xml_ReaderName ( p_xml_reader ); + psz_value = xml_ReaderValue ( p_xml_reader ); + if ( !psz_name || !psz_value ) + { + msg_Err( p_demux, "invalid xml stream @ <playlist>" ); + FREE_ATT(); + return VLC_FALSE; + } + /* attribute: version */ + if ( !strcmp( psz_name, "version" ) ) + { + b_version_found = VLC_TRUE; + if ( strcmp( psz_value, "0" ) && strcmp( psz_value, "1" ) ) + msg_Warn( p_demux, "unsupported XSPF version" ); + } + /* attribute: xmlns */ + else if ( !strcmp ( psz_name, "xmlns" ) ) + ; + /* unknown attribute */ + else + msg_Warn( p_demux, "invalid <playlist> attribute:\"%s\"", psz_name ); + + FREE_ATT(); + } + /* attribute version is mandatory !!! */ + if ( !b_version_found ) + msg_Warn( p_demux, "<playlist> requires \"version\" attribute" ); + + /* parse the child elements - we only take care of <trackList> */ + while ( xml_ReaderRead( p_xml_reader ) == 1 ) + { + i_node = xml_ReaderNodeType( p_xml_reader ); + switch ( i_node ) + { + case XML_READER_NONE: + break; + case XML_READER_STARTELEM: + /* element start tag */ + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name || !*psz_name ) + { + msg_Err( p_demux, "invalid xml stream" ); + FREE_ATT(); + return VLC_FALSE; + } + /* choose handler */ + for( p_handler = pl_elements; + p_handler->name && strcmp( psz_name, p_handler->name ); + p_handler++ ); + if ( !p_handler->name ) + { + msg_Err( p_demux, "unexpected element <%s>", psz_name ); + FREE_ATT(); + return VLC_FALSE; + } + FREE_NAME(); + /* complex content is parsed in a separate function */ + if ( p_handler->type == COMPLEX_CONTENT ) + { + if ( p_handler->pf_handler.cmplx( p_demux, + p_playlist, + p_item, + p_xml_reader, + p_handler->name ) ) + { + p_handler = NULL; + FREE_ATT(); + } else + { + FREE_ATT(); + return VLC_FALSE; + } + } + break; + + case XML_READER_TEXT: + /* simple element content */ + FREE_ATT(); + psz_value = xml_ReaderValue( p_xml_reader ); + if ( !psz_value ) + { + msg_Err( p_demux, "invalid xml stream" ); + FREE_ATT(); + return VLC_FALSE; + } + break; + + case XML_READER_ENDELEM: + /* element end tag */ + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name ) + { + msg_Err( p_demux, "invalid xml stream" ); + FREE_ATT(); + return VLC_FALSE; + } + /* leave if the current parent node <playlist> is terminated */ + if ( !strcmp( psz_name, psz_element ) ) + { + FREE_ATT(); + return VLC_TRUE; + } + /* there MUST have been a start tag for that element name */ + if ( !p_handler || !p_handler->name || strcmp( p_handler->name, psz_name )) + { + msg_Err( p_demux, "there's no open element left for <%s>", psz_name ); + FREE_ATT(); + return VLC_FALSE; + } + + if ( p_handler->pf_handler.smpl ) + { + p_handler->pf_handler.smpl( p_item, p_handler->name, psz_value ); + } + FREE_ATT(); + p_handler = NULL; + break; + + default: + /* unknown/unexpected xml node */ + msg_Err( p_demux, "unexpected xml node %i", i_node ); + FREE_ATT(); + return VLC_FALSE; + break; + } + FREE_NAME(); + } + return VLC_FALSE; +} + +/** + * \brief parses the tracklist node which only may contain <track>s + */ +static vlc_bool_t parse_tracklist_node COMPLEX_INTERFACE +{ + char *psz_name=NULL; + int i_node; + int i_ntracks = 0; + + /* now parse the <track>s */ + while ( xml_ReaderRead( p_xml_reader ) == 1 ) + { + i_node = xml_ReaderNodeType( p_xml_reader ); + if ( i_node == XML_READER_STARTELEM ) + { + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name ) + { + msg_Err( p_demux, "unexpected end of xml data" ); + FREE_NAME(); + return VLC_FALSE; + } + if ( strcmp( psz_name, "track") ) + { + msg_Err( p_demux, "unexpected child of <trackList>: <%s>", psz_name ); + FREE_NAME(); + return VLC_FALSE; + } + FREE_NAME(); + + /* parse the track data in a separate function */ + if ( parse_track_node( p_demux, p_playlist, p_item, p_xml_reader, "track" ) + == VLC_TRUE ) + i_ntracks++; + } else if ( i_node == XML_READER_ENDELEM ) + break; + } + + /* the <trackList> has to be terminated */ + if ( xml_ReaderNodeType( p_xml_reader ) != XML_READER_ENDELEM ) + { + msg_Err( p_demux, "there's a missing </trackList>" ); + FREE_NAME(); + return VLC_FALSE; + } + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name || strcmp( psz_name, "trackList" ) ) + { + msg_Err( p_demux, "expected: </trackList>, found: </%s>", psz_name ); + FREE_NAME(); + return VLC_FALSE; + } + FREE_NAME(); + + msg_Dbg( p_demux, "parsed %i tracks successfully", i_ntracks ); + + return VLC_TRUE; +} + +/** + * \brief parse one track element + * \param COMPLEX_INTERFACE + */ +static vlc_bool_t parse_track_node COMPLEX_INTERFACE +{ + playlist_item_t *p_new=NULL; + int i_node; + char *psz_name=NULL; + char *psz_value=NULL; + xml_elem_hnd_t *p_handler=NULL; + + xml_elem_hnd_t track_elements[] = + { {"location", SIMPLE_CONTENT, {NULL} }, + {"identifier", SIMPLE_CONTENT, {NULL} }, + {"title", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"creator", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"annotation", SIMPLE_CONTENT, {NULL} }, + {"info", SIMPLE_CONTENT, {NULL} }, + {"image", SIMPLE_CONTENT, {NULL} }, + {"album", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"trackNum", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"duration", SIMPLE_CONTENT, {.smpl = set_item_info} }, + {"link", SIMPLE_CONTENT, {NULL} }, + {"meta", SIMPLE_CONTENT, {NULL} }, + {"extension", COMPLEX_CONTENT, {.cmplx = skip_element} }, + {NULL, UNKNOWN_CONTENT, {NULL} } + }; + + while ( xml_ReaderRead( p_xml_reader ) == 1 ) + { + i_node = xml_ReaderNodeType( p_xml_reader ); + switch ( i_node ) + { + case XML_READER_NONE: + break; + + case XML_READER_STARTELEM: + /* element start tag */ + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name || !*psz_name ) + { + msg_Err( p_demux, "invalid xml stream" ); + FREE_ATT(); + return VLC_FALSE; + } + /* choose handler */ + for( p_handler = track_elements; + p_handler->name && strcmp( psz_name, p_handler->name ); + p_handler++ ); + if ( !p_handler->name ) + { + msg_Err( p_demux, "unexpected element <%s>", psz_name ); + FREE_ATT(); + return VLC_FALSE; + } + FREE_NAME(); + /* complex content is parsed in a separate function */ + if ( p_handler->type == COMPLEX_CONTENT ) + { + if ( !p_new ) + { + msg_Err( p_demux, "at <%s> level no new item has been allocated", p_handler->name ); + FREE_ATT(); + return VLC_FALSE; + } + if ( p_handler->pf_handler.cmplx( p_demux, + p_playlist, + p_new, + p_xml_reader, + p_handler->name ) ) + { + p_handler = NULL; + FREE_ATT(); + } else + { + FREE_ATT(); + return VLC_FALSE; + } + } + break; + + case XML_READER_TEXT: + /* simple element content */ + FREE_ATT(); + psz_value = xml_ReaderValue( p_xml_reader ); + if ( !psz_value ) + { + msg_Err( p_demux, "invalid xml stream" ); + FREE_ATT(); + return VLC_FALSE; + } + break; + + case XML_READER_ENDELEM: + /* element end tag */ + psz_name = xml_ReaderName( p_xml_reader ); + if ( !psz_name ) + { + msg_Err( p_demux, "invalid xml stream" ); + FREE_ATT(); + return VLC_FALSE; + } + /* leave if the current parent node <track> is terminated */ + if ( !strcmp( psz_name, psz_element ) ) + { + FREE_ATT(); + return VLC_TRUE; + } + /* there MUST have been a start tag for that element name */ + if ( !p_handler || !p_handler->name || strcmp( p_handler->name, psz_name )) + { + msg_Err( p_demux, "there's no open element left for <%s>", psz_name ); + FREE_ATT(); + return VLC_FALSE; + } + + /* special case: location */ + if ( !strcmp( p_handler->name, "location" ) ) + { + /* there MUST NOT be an item */ + if ( p_new ) + { + msg_Err( p_demux, "a new item has just been created <%s>", psz_name ); + FREE_ATT(); + return VLC_FALSE; + } + /* create it now */ + if ( insert_new_item( p_playlist, p_item, &p_new, psz_value ) ) + { + FREE_ATT(); + p_handler = NULL; + } else + { + FREE_ATT(); + return VLC_FALSE; + } + } + else + { + /* there MUST be an item */ + if ( !p_new ) + { + msg_Err( p_demux, "an item hasn't been created yet <%s>", psz_name ); + FREE_ATT(); + return VLC_FALSE; + } + if ( p_handler->pf_handler.smpl ) + { + p_handler->pf_handler.smpl( p_new, p_handler->name, psz_value ); + FREE_ATT(); + } + } + FREE_ATT(); + p_handler = NULL; + break; + + default: + /* unknown/unexpected xml node */ + msg_Err( p_demux, "unexpected xml node %i", i_node ); + FREE_ATT(); + return VLC_FALSE; + break; + } + FREE_NAME(); + } + msg_Err( p_demux, "unexpected end of xml data" ); + FREE_ATT(); + return VLC_FALSE; +} + +/** + * \brief handles the supported <track> sub-elements + */ +static vlc_bool_t set_item_info SIMPLE_INTERFACE +{ + /* exit if setting is impossible */ + if ( !psz_name || !psz_value || !p_item ) + return VLC_FALSE; + + /* re-convert xml special characters inside psz_value */ + resolve_xml_special_chars ( psz_value ); + + /* handle each info element in a separate "if" clause */ + if ( !strcmp( psz_name, "title" ) ) + { + if ( playlist_ItemSetName ( p_item, (char *)psz_value ) == VLC_SUCCESS ) + return VLC_TRUE; + return VLC_FALSE; + + } else if ( !strcmp( psz_name, "creator" ) ) + { + if ( vlc_input_item_AddInfo( &(p_item->input), + _(VLC_META_INFO_CAT), _(VLC_META_ARTIST), + "%s", psz_value ) == VLC_SUCCESS ) + return VLC_TRUE; + return VLC_FALSE; + + } else if ( !strcmp( psz_name, "album" ) ) + { + if ( vlc_input_item_AddInfo( &(p_item->input), + _(VLC_META_INFO_CAT), _(VLC_META_COLLECTION), + "%s", psz_value ) == VLC_SUCCESS ) + return VLC_TRUE; + return VLC_FALSE; + + } else if ( !strcmp( psz_name, "trackNum" ) ) + { + long i_num = atol( psz_value ); + if ( i_num > 0 ) + if ( vlc_input_item_AddInfo( &(p_item->input), + _(VLC_META_INFO_CAT), _(VLC_META_SEQ_NUM), + "%s", psz_value ) == VLC_SUCCESS ) + return VLC_TRUE; + return VLC_FALSE; + + } else if ( !strcmp( psz_name, "duration" ) ) + { + long i_num = atol( psz_value ); + if ( i_num > 0 ) + if ( playlist_ItemSetDuration ( p_item, i_num*1000 ) == VLC_SUCCESS ) + return VLC_TRUE; + return VLC_FALSE; + + } + + return VLC_TRUE; +} + +/** + * \brief skips complex element content that we can't manage + */ +static vlc_bool_t skip_element COMPLEX_INTERFACE +{ + char *psz_endname; + + while ( xml_ReaderRead( p_xml_reader ) == 1 ) + { + if ( xml_ReaderNodeType( p_xml_reader ) == XML_READER_ENDELEM ) + { + psz_endname = xml_ReaderName( p_xml_reader ); + if ( !psz_endname ) + return VLC_FALSE; + if ( !strcmp( psz_element, psz_endname ) ) + { + free( psz_endname ); + return VLC_TRUE; + } else + free( psz_endname ); + } + } + return VLC_FALSE; +} + +/** + * \brief creates a new playlist item from the given mrl + */ +static vlc_bool_t insert_new_item( playlist_t *p_pl, playlist_item_t *p_cur, + playlist_item_t **pp_new, char *psz_location ) +{ + char *psz_uri=NULL; + psz_uri = uri_percent_decode( psz_location ); + + if ( psz_uri ) + { + *pp_new = playlist_ItemNew( p_pl, psz_uri, NULL ); + free( psz_uri ); + psz_uri = NULL; + } + + if ( !*pp_new ) + return VLC_FALSE; + + playlist_NodeAddItem( p_pl, *pp_new, p_cur->pp_parents[0]->i_view, + p_cur, PLAYLIST_APPEND, PLAYLIST_END ); + + playlist_CopyParents( p_cur, *pp_new ); + + vlc_input_item_CopyOptions( &p_cur->input, &((*pp_new)->input) ); + + return VLC_TRUE; +} + +/** + * \brief nomen est omen + * \param psz_in the string may contain percent-encoded characters + * \return a new allocated buffer with the decoded percent-encoded characters + */ +static char *uri_percent_decode( const char *psz_in ) +{ + const char *hexchars="0123456789ABCDEFabcdef"; + + char *psz_ret; + const char *src; + char *dst; + char *pc1, *pc2; + + if ( !psz_in || !*psz_in ) + return NULL; + + psz_ret = malloc( sizeof(char) * strlen( psz_in ) + 1 ); + if ( !psz_ret ) + return NULL; + + for ( src = psz_in, dst = psz_ret; *src; dst++, src++ ) + { + if ( *src != '%' ) + *dst = *src; + else + { + pc1 = strchr( hexchars, *(src+1) ); + if ( *(src+1) ) /* the string could end in "...%" */ + pc2 = strchr( hexchars, *(src+2) ); + else + pc2 = NULL; + + if ( pc1 && pc2 ) + { + /* map a matching lower-case letter to upper-case index */ + if ( pc1 - hexchars > 15 ) + pc1 -= 6; + if ( pc2 - hexchars > 15 ) + pc2 -= 6; + *dst = (pc1 - hexchars) * 16 + (pc2 - hexchars); + src += 2; + } + } + } + *dst = '\0'; + + /** \note i don't realloc() the string, because it would need extra time + * and generally save only a few bytes. + */ + return psz_ret; +} + +/** + * \brief converts "<", ">" and "&" to "<", ">" and "&" + * \param string to convert + */ +static void resolve_xml_special_chars( char *psz_value ) +{ + char *p_pos = psz_value; + + while ( *psz_value ) + { + if ( !strncmp( psz_value, "<", 4 ) ) + { + *p_pos = '<'; + psz_value += 4; + } else if ( !strncmp( psz_value, ">", 4 ) ) + { + *p_pos = '>'; + psz_value += 4; + } else if ( !strncmp( psz_value, "&", 5 ) ) + { + *p_pos = *psz_value; + psz_value += 5; + } else + { + *p_pos = *psz_value; + psz_value++; + } + + p_pos++; + } + + *p_pos = '\0'; +} Index: modules/demux/playlist/playlist.c =================================================================== --- modules/demux/playlist/playlist.c (Revision 14333) +++ modules/demux/playlist/playlist.c (Arbeitskopie) @@ -84,6 +84,11 @@ add_shortcut( "podcast" ); set_capability( "demux2", 10 ); set_callbacks( E_(Import_podcast), E_(Close_podcast) ); + add_submodule(); + set_description( _("XSPF playlist import") ); + add_shortcut( "xspf-open" ); + set_capability( "demux2", 10 ); + set_callbacks( E_(xspf_import_Activate), NULL ); vlc_module_end(); Index: modules/demux/playlist/Modules.am =================================================================== --- modules/demux/playlist/Modules.am (Revision 14333) +++ modules/demux/playlist/Modules.am (Arbeitskopie) @@ -1,8 +1,14 @@ -SOURCES_playlist = playlist.c \ - playlist.h \ - old.c \ - m3u.c \ - b4s.c \ - pls.c \ - dvb.c \ - podcast.c +SOURCES_playlist = \ + playlist.c \ + playlist.h \ + old.c \ + m3u.c \ + b4s.c \ + pls.c \ + dvb.c \ + podcast.c \ + xspf.c \ + xspf.h \ + $(NULL) + + Index: modules/demux/playlist/xspf.h =================================================================== --- modules/demux/playlist/xspf.h (Revision 0) +++ modules/demux/playlist/xspf.h (Revision 0) @@ -0,0 +1,64 @@ +/***************************************************************************** + * Copyright (C) 2006 Daniel Stränger <vlc at schmaller dot de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *******************************************************************************/ +/** + * \file modules/demux/playlist/xspf.h + * \brief XSPF playlist import: prototypes, datatypes, defines + */ + +/* defines */ +#define FREE_NAME() if (psz_name) {free(psz_name);psz_name=NULL;} +#define FREE_VALUE() if (psz_value) {free(psz_value);psz_value=NULL;} +#define FREE_ATT() FREE_NAME();FREE_VALUE() + +#define UNKNOWN_CONTENT 0 +#define SIMPLE_CONTENT 1 +#define COMPLEX_CONTENT 2 + +#define SIMPLE_INTERFACE (playlist_item_t *p_item,\ + const char *psz_name,\ + char *psz_value) +#define COMPLEX_INTERFACE (demux_t *p_demux,\ + playlist_t *p_playlist,\ + playlist_item_t *p_item,\ + xml_reader_t *p_xml_reader,\ + const char *psz_element) + +/* prototypes */ +int xspf_import_Demux( demux_t *); +int xspf_import_Control( demux_t *, int, va_list ); + +static vlc_bool_t parse_playlist_node COMPLEX_INTERFACE; +static vlc_bool_t parse_tracklist_node COMPLEX_INTERFACE; +static vlc_bool_t parse_track_node COMPLEX_INTERFACE; +static vlc_bool_t set_item_info SIMPLE_INTERFACE; +static vlc_bool_t skip_element COMPLEX_INTERFACE; +static vlc_bool_t insert_new_item( playlist_t *, playlist_item_t *, playlist_item_t **, char *); +static char *uri_percent_decode( const char * ); +static void resolve_xml_special_chars( char * ); + +/* datatypes */ +typedef struct +{ + const char *name; + int type; + union + { + vlc_bool_t (*smpl) SIMPLE_INTERFACE; + vlc_bool_t (*cmplx) COMPLEX_INTERFACE; + } pf_handler; +} xml_elem_hnd_t; Index: modules/demux/playlist/playlist.h =================================================================== --- modules/demux/playlist/playlist.h (Revision 14333) +++ modules/demux/playlist/playlist.h (Arbeitskopie) @@ -45,3 +45,5 @@ int E_(Import_podcast) ( vlc_object_t * ); void E_(Close_podcast) ( vlc_object_t * ); + +int E_(xspf_import_Activate) ( vlc_object_t * ); Index: modules/gui/wxwidgets/dialogs/playlist.cpp =================================================================== --- modules/gui/wxwidgets/dialogs/playlist.cpp (Revision 14333) +++ modules/gui/wxwidgets/dialogs/playlist.cpp (Arbeitskopie) @@ -900,7 +900,9 @@ char *psz_desc; char *psz_filter; char *psz_module; - } formats[] = {{ _("M3U file"), "*.m3u", "export-m3u" }}; + } formats[] = {{ _("M3U file"), "*.m3u", "export-m3u" }, + { _("XSPF playlist"), "*.xspf", "export-xspf"} + }; wxString filter = wxT(""); @@ -935,7 +937,7 @@ void Playlist::OnOpen( wxCommandEvent& WXUNUSED(event) ) { wxFileDialog dialog( this, wxU(_("Open playlist")), wxT(""), wxT(""), - wxT("All playlists|*.pls;*.m3u;*.asx;*.b4s|M3U files|*.m3u"), wxOPEN ); + wxT("All playlists|*.pls;*.m3u;*.asx;*.b4s;*.xspf|XSPF playlist|*.xspf|M3U files|*.m3u"), wxOPEN ); if( dialog.ShowModal() == wxID_OK ) { Index: modules/gui/skins2/src/dialogs.cpp =================================================================== --- modules/gui/skins2/src/dialogs.cpp (Revision 14333) +++ modules/gui/skins2/src/dialogs.cpp (Arbeitskopie) @@ -229,14 +229,16 @@ void Dialogs::showPlaylistLoad() { showFileGeneric( _("Open playlist"), - _("All playlists|*.pls;*.m3u;*.asx;*.b4s|M3U files|*.m3u"), + _("All playlists|*.pls;*.m3u;*.asx;*.b4s;*.xspf|" + "M3U files|*.m3u|" + "XSPF playlist|*.xspf"), showPlaylistLoadCB, kOPEN ); } void Dialogs::showPlaylistSave() { - showFileGeneric( _("Save playlist"), _("M3U file|*.m3u"), + showFileGeneric( _("Save playlist"), _("M3U file|*.m3u|XSPF playlist|*.xspf"), showPlaylistSaveCB, kSAVE ); } Index: modules/misc/playlist/xspf.c =================================================================== --- modules/misc/playlist/xspf.c (Revision 0) +++ modules/misc/playlist/xspf.c (Revision 0) @@ -0,0 +1,363 @@ +/****************************************************************************** + * Copyright (C) 2006 Daniel Stränger <vlc at schmaller dot de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *******************************************************************************/ +/** + * \file modules/misc/playlist/xspf.c + * \brief XSPF playlist export functions + */ +#include <stdio.h> +#include <vlc/vlc.h> +#include <vlc/intf.h> +#include "vlc_meta.h" +#include "xspf.h" + +/* static pointer to char buffer to avoid repeating mallocs and frees */ +static char *psz_temp = NULL; + +/** + * \brief Prints the XSPF header to file, writes each item by xspf_export_item() + * and closes the open xml elements + * \param p_this the VLC playlist object + * \return VLC_SUCCESS if some memory is available, otherwise VLC_ENONMEM + */ +int E_(xspf_export_playlist)( vlc_object_t *p_this ) +{ + const playlist_t *p_playlist = (playlist_t *)p_this; + const playlist_export_t *p_export = (playlist_export_t *)p_playlist->p_private; + int i; + char *psz; + playlist_item_t **pp_items = NULL; + int i_size; + playlist_item_t *p_node; + + /* allocate char buffer - the longest replacement is "&", i.e. 5 times */ + psz_temp = malloc( sizeof(char) * XSPF_MAX_CONTENT*5 + 1); + if (!psz_temp) + { + msg_Err( p_playlist, "Not enough memory: %d", XSPF_MAX_CONTENT*5+1 ); + return VLC_ENOMEM; + } + + /* write XSPF XML header - since we don't use <extension>, we get by with version 0 */ + fprintf( p_export->p_file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ); + fprintf( p_export->p_file, "<playlist version=\"0\" xmlns=\"http://xspf.org/ns/0/\">\n" ); + + /* save tho whole playlist or only the current node */ + if ( p_playlist->status.p_item ) + for (i = 0; i < p_playlist->status.p_item->i_parents; i++ ) + if ( p_playlist->status.p_item->pp_parents[i]->p_parent->input.i_type == ITEM_TYPE_PLAYLIST ) + { + /* set the current node and its children */ + p_node = p_playlist->status.p_item->pp_parents[i]->p_parent; + pp_items = p_node->pp_children; + i_size = p_node->i_children; + + /* save name of the playlist node */ + *psz_temp = '\0'; + convert_xml_special_chars( p_node->input.psz_name ); + if ( *psz_temp ) + fprintf( p_export->p_file, "\t<title>%s</title>\n", psz_temp ); + + /* save the creator of the playlist node */ + *psz_temp = '\0'; + psz = vlc_input_item_GetInfo( &p_node->input, + _(VLC_META_INFO_CAT), _(VLC_META_ARTIST) ); + if ( psz && !*psz ) + { + free ( psz ); + psz = NULL; + } + if ( !psz ) + psz = vlc_input_item_GetInfo( &p_node->input, + _(VLC_META_INFO_CAT), _(VLC_META_AUTHOR) ); + convert_xml_special_chars( psz ); + if ( psz ) free( psz ); + if ( *psz_temp ) + fprintf( p_export->p_file, "\t<creator>%s</creator>\n", psz_temp ); + + /* save location of the playlist node */ + psz = assertUTF8URI( p_export->psz_filename ); + if ( psz && *psz ) + { + fprintf( p_export->p_file, "\t<location>%s</location>\n", psz ); + free( psz ); + } + break; + } + + /* prepare all the playlist children for export */ + if ( !pp_items ) + { + pp_items = p_playlist->pp_items; + i_size = p_playlist->i_size; + } + + /* export all items */ + fprintf( p_export->p_file, "\t<trackList>\n" ); + for ( i = 0; i < i_size; i++ ) + { + xspf_export_item( pp_items[i], p_export->p_file ); + } + + /* close the header elements */ + fprintf( p_export->p_file, "\t</trackList>\n" ); + fprintf( p_export->p_file, "</playlist>\n" ); + + free(psz_temp); + psz_temp = NULL; + return VLC_SUCCESS; +} + +/** + * \brief exports one item to file or traverse if item is a node + * \param p_item playlist item to export + * \param p_file file to write xml-converted item to + */ +static void xspf_export_item( playlist_item_t *p_item, FILE *p_file ) +{ + int i; /**< iterator for all children if the current item is a node */ + char *psz; + + if ( !p_item ) + return; + + /** \todo only "flat" playlists supported at this time. + extend to save the tree structure. + */ + /* if we get a node here, we must traverse it */ + if ( p_item->i_children > 0 ) + { + for ( i = 0; i < p_item->i_children; i++ ) + { + xspf_export_item( p_item->pp_children[i], p_file ); + } + return; + } + + /* leaves can be written directly */ + fprintf( p_file, "\t\t<track>\n" ); + + /* -> the location */ + if ( p_item->input.psz_uri && *p_item->input.psz_uri ) + { + psz = assertUTF8URI( p_item->input.psz_uri ); + fprintf( p_file, "\t\t\t<location>%s</location>\n", psz ); + free( psz ); + } + + /* -> the name/title (only if different from uri)*/ + *psz_temp = '\0'; + if ( p_item->input.psz_name && + p_item->input.psz_uri && + strcmp( p_item->input.psz_uri, p_item->input.psz_name ) ) + convert_xml_special_chars( p_item->input.psz_name ); + if ( *psz_temp ) + fprintf( p_file, "\t\t\t<title>%s</title>\n", psz_temp ); + + /* -> the artist/creator */ + *psz_temp = '\0'; + psz = vlc_input_item_GetInfo( &p_item->input, + _(VLC_META_INFO_CAT), _(VLC_META_ARTIST) ); + if ( psz && !*psz ) + { + free ( psz ); + psz = NULL; + } + if ( !psz ) + psz = vlc_input_item_GetInfo( &p_item->input, + _(VLC_META_INFO_CAT), _(VLC_META_AUTHOR) ); + convert_xml_special_chars( psz ); + if ( psz ) free( psz ); + if ( *psz_temp ) + fprintf( p_file, "\t\t\t<creator>%s</creator>\n", psz_temp ); + + /* -> the album */ + *psz_temp = '\0'; + psz = vlc_input_item_GetInfo( &p_item->input, + _(VLC_META_INFO_CAT), _(VLC_META_COLLECTION) ); + convert_xml_special_chars( psz ); + if ( psz ) free( psz ); + if ( *psz_temp ) + fprintf( p_file, "\t\t\t<album>%s</album>\n", psz_temp ); + + /* -> the track number */ + psz = vlc_input_item_GetInfo( &p_item->input, + _(VLC_META_INFO_CAT), _(VLC_META_SEQ_NUM) ); + if ( psz ) + { + if ( *psz ) + fprintf( p_file, "\t\t\t<trackNum>%i</trackNum>\n", atoi( psz ) ); + free( psz ); + } + + /* -> the duration */ + if ( p_item->input.i_duration > 0 ) + { + fprintf( p_file, "\t\t\t<duration>%ld</duration>\n", + (long)(p_item->input.i_duration / 1000) ); + } + + fprintf( p_file, "\t\t</track>\n" ); + + return; +} + +/** + * \param psz_name the location of the media ressource (e.g. local file, device, network stream, etc.) + * \return a new char buffer which asserts that the location is valid UTF-8 and a valid URI + * \note the returned buffer must be freed, when it isn't used anymore + */ +static char *assertUTF8URI( char *psz_name ) +{ + char *psz_utf8 = NULL; /**< the new UTF-8 string, if conversion is neccessary */ + char *psz_ret = NULL; /**< the new result buffer to return */ + char *psz_s = NULL, *psz_d = NULL; /**< src & dest pointers for URI conversion */ + vlc_bool_t b_name_is_uri = VLC_FALSE; + + if ( !psz_name || !*psz_name ) + return NULL; + + /* max. 6x for UTF-8 conversion and + max. 3x for URI conversion (percent escaping) and + 8 bytes for "file://" and NULL-termination */ + psz_ret = (char *)malloc( sizeof(char)*strlen(psz_name)*6*3+8 ); + if ( !psz_ret ) + return NULL; + + /* convert string to UTF-8, if it isn't yet valid UTF-8 */ + if ( checkUTF8( psz_name ) ) + { + psz_s = psz_name; + } + else + { + psz_utf8 = FromLocale( psz_name ); + psz_s = psz_utf8; + } + + /** \todo check for a valid scheme part preceding the colon */ + if ( strchr( psz_s, ':' ) ) + { + psz_d = psz_ret; + b_name_is_uri = VLC_TRUE; + } + /* assume "file" scheme if no scheme-part is included */ + else + { + strcpy( psz_ret, "file://" ); + psz_d = psz_ret + 7; + } + + while ( *psz_s ) + { + /* percent-encode all non-ASCII and the XML special characters and the percent sign itself */ + if ( *psz_s & B10000000 || + *psz_s == '<' || + *psz_s == '>' || + *psz_s == '&' || + *psz_s == ' ' || + ( *psz_s == '%' && !b_name_is_uri ) ) + { + *psz_d++ = '%'; + *psz_d++ = hexchars[(*psz_s >> 4) & B00001111]; + *psz_d++ = hexchars[*psz_s & B00001111]; + } else + *psz_d++ = *psz_s; + + psz_s++; + } + *psz_d = '\0'; + + if ( psz_utf8 ) + free ( psz_utf8 ); + + return (char *)realloc( psz_ret, sizeof(char)*strlen( psz_ret ) + 1 ); +} + +/** + * \brief converts '<', '>' and '&' to its html entities + * \param psz_content simple element content that is to be converted + */ +static void convert_xml_special_chars(const char *psz_content) +{ + const char *p_from = psz_content; + char *p_to = psz_temp; + if ( psz_content && + strlen( psz_content ) <= XSPF_MAX_CONTENT ) + while ( *p_from ) + { + if ( *p_from == '<' ) + { + strcpy( p_to, "<" ); + p_to += 4; + } else if ( *p_from == '>' ) + { + strcpy( p_to, ">" ); + p_to += 4; + } else if ( *p_from == '&' ) + { + strcpy( p_to, "&" ); + p_to += 5; + } else + { + *p_to = *p_from; + p_to++; + } + p_from++; + } + *p_to = '\0'; +} + +/** + * \brief checks if the string is a valid UTF-8 string + * \param null-terminated string buffer to check + * \return true if the string is valid UTF-8, false otherwise + */ +static vlc_bool_t checkUTF8( char *psz_utf8 ) +{ + unsigned char c; + + while ( *psz_utf8 ) + { + /* ASCII character -> ok, check next */ + if ( !(*psz_utf8 & B10000000) ) + { + psz_utf8++; + continue; + } + + /* non-ASCII and not UTF-8 initial character -> non-UTF8 string */ + if ( !(*psz_utf8 & B01000000) ) + return VLC_FALSE; + + /* start UTF8 processing */ + c = *psz_utf8 << 1; + psz_utf8++; + while ( c & B10000000 ) + { + if ((*psz_utf8 & B11000000) == B10000000) + { + c <<= 1; + psz_utf8++; + } + else + return VLC_FALSE; + } + } + return VLC_TRUE; +} + Index: modules/misc/playlist/export.c =================================================================== --- modules/misc/playlist/export.c (Revision 14333) +++ modules/misc/playlist/export.c (Arbeitskopie) @@ -31,8 +31,8 @@ ***************************************************************************/ int Export_M3U ( vlc_object_t *p_intf ); int Export_Old ( vlc_object_t *p_intf ); +int E_(xspf_export_playlist)( vlc_object_t *p_intf ); - /***************************************************************************** * Module descriptor *****************************************************************************/ @@ -52,4 +52,10 @@ set_capability( "playlist export" , 0); set_callbacks( Export_Old , NULL ); + add_submodule(); + set_description( _("XSPF playlist export") ); + add_shortcut( "export-xspf" ); + set_capability( "playlist export" , 0); + set_callbacks( E_(xspf_export_playlist) , NULL ); + vlc_module_end(); Index: modules/misc/playlist/Modules.am =================================================================== --- modules/misc/playlist/Modules.am (Revision 14333) +++ modules/misc/playlist/Modules.am (Arbeitskopie) @@ -1,3 +1,7 @@ -SOURCES_export = export.c \ - m3u.c \ - old.c +SOURCES_export = \ + export.c \ + m3u.c \ + xspf.c \ + xspf.h \ + old.c \ + $(NULL) Index: modules/misc/playlist/xspf.h =================================================================== --- modules/misc/playlist/xspf.h (Revision 0) +++ modules/misc/playlist/xspf.h (Revision 0) @@ -0,0 +1,39 @@ +/***************************************************************************** + * Copyright (C) 2006 Daniel Stränger <vlc at schmaller dot de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *******************************************************************************/ +/** + * \file modules/misc/playlist/xspf.h + * \brief XSPF playlist export module header file + */ + +/* defs */ +#define B10000000 0x80 +#define B01000000 0x40 +#define B11000000 0xc0 +#define B00001111 0x0f + +#define XSPF_MAX_CONTENT 2000 + +/* constants */ +const char hexchars[16] = "0123456789ABCDEF"; + +/* prototypes */ +int E_(xspf_export_playlist)( vlc_object_t * ); +static void xspf_export_item( playlist_item_t *, FILE * ); +static char *assertUTF8URI( char * ); +static void convert_xml_special_chars( const char * ); +static vlc_bool_t checkUTF8( char * ); Index: modules/access/http.c =================================================================== --- modules/access/http.c (Revision 14333) +++ modules/access/http.c (Arbeitskopie) @@ -402,6 +402,9 @@ /* Grrrr! detect ultravox server and force NSV demuxer */ p_access->psz_demux = strdup( "nsv" ); } + else if( p_sys->psz_mime && + !strcasecmp( p_sys->psz_mime, "application/xspf+xml" ) ) + p_access->psz_demux = strdup( "xspf-open" ); if( p_sys->b_reconnect ) msg_Dbg( p_access, "auto re-connect enabled" );
- hartman added Difficulty::medium label
added Difficulty::medium label
- Migration Bot changed title from New playlist format: XSPF to XSPF support
changed title from New playlist format: XSPF to XSPF support
- zorglub added Status::fixed label
added Status::fixed label
- zorglub closed
closed