/***************************************************************************** * m3u.c : M3U playlist format import ***************************************************************************** * Copyright (C) 2004 VLC authors and VideoLAN * * Authors: Clément Stenac * Sigmund Augdal Helberg * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser 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. *****************************************************************************/ /***************************************************************************** * Preamble *****************************************************************************/ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include "playlist.h" /***************************************************************************** * Local prototypes *****************************************************************************/ static int ReadDir( stream_t *, input_item_node_t * ); static bool ContainsURL(const uint8_t *, size_t); static char *GuessEncoding (const char *str) { return IsUTF8 (str) ? strdup (str) : FromLatin1 (str); } static char *CheckUnicode (const char *str) { return IsUTF8 (str) ? strdup (str): NULL; } /***************************************************************************** * Import_M3U: main import function *****************************************************************************/ int Import_M3U( vlc_object_t *p_this ) { stream_t *p_stream = (stream_t *)p_this; const uint8_t *p_peek; ssize_t i_peek; unsigned int offset = 0; i_peek = vlc_stream_Peek( p_stream->s, &p_peek, 1024 ); if( i_peek < 8 ) return VLC_EGENERIC; /* Encoding: UTF-8 or unspecified */ char *(*pf_dup) (const char *) = GuessEncoding; if (memcmp( p_peek, "\xef\xbb\xbf", 3) == 0) /* UTF-8 Byte Order Mark */ { if( i_peek < 12 ) return VLC_EGENERIC; pf_dup = CheckUnicode; offset = 3; p_peek += offset; i_peek -= offset; } if (stream_HasExtension(p_stream, ".m3u8") || strncasecmp((const char *)p_peek, "RTSPtext", 8) == 0) /* QuickTime */ pf_dup = CheckUnicode; /* File type: playlist, or not (HLS manifest or whatever else) */ char *type = stream_MimeType(p_stream->s); bool match; if (!p_stream->obj.force && memcmp(p_peek, "#EXTM3U", 7 ) != 0 && (type == NULL || (vlc_ascii_strcasecmp(type, "application/mpegurl") != 0 && vlc_ascii_strcasecmp(type, "application/x-mpegurl") != 0 && vlc_ascii_strcasecmp(type, "audio/mpegurl") != 0 && vlc_ascii_strcasecmp(type, "vnd.apple.mpegURL") != 0 && vlc_ascii_strcasecmp(type, "audio/x-mpegurl") != 0)) && !stream_HasExtension(p_stream, ".m3u8") && !stream_HasExtension(p_stream, ".m3u") && !stream_HasExtension(p_stream, ".vlc") && strncasecmp((const char *)p_peek, "RTSPtext", 8) != 0 && !ContainsURL(p_peek, i_peek)) match = false; else match = true; free(type); if (!match) return VLC_EGENERIC; if (offset && vlc_stream_Read(p_stream->s, NULL, offset) != offset) return VLC_EGENERIC; msg_Dbg( p_stream, "found valid M3U playlist" ); p_stream->p_sys = pf_dup; p_stream->pf_readdir = ReadDir; p_stream->pf_control = PlaylistControl; return VLC_SUCCESS; } static bool ContainsURL(const uint8_t *p_peek, size_t i_peek) { const char *ps = (const char *)p_peek; const char *ps_end = (const char *) p_peek + i_peek; const size_t i_max = sizeof( "https://" ); if( i_peek < i_max + 1 ) return false; bool b_newline = true; while( ps + i_max + 1 < ps_end ) { if( *ps <= 0 ) return false; /* Goto next line */ if( *ps == '\n' ) { ps++; b_newline = true; continue; } /* One line starting with a URL is enough */ if( b_newline ) { const char *ps_match = strnstr( ps, "://", i_max ); if(ps_match) { switch(ps_match - ps) { case 3: if( !strncasecmp( ps, "mms", 3 ) || !strncasecmp( ps, "ftp", 3 ) ) return true; break; case 4: if( !strncasecmp( ps, "http", 4 ) || !strncasecmp( ps, "rtsp", 4 ) || !strncasecmp( ps, "ftps", 4 ) ) return true; break; case 5: if( !strncasecmp( ps, "https", 5 ) || !strncasecmp( ps, "ftpes", 5 ) ) return true; default: break; } } /* Comments and blank lines are ignored */ if( *ps != '#' && *ps != '\n' && *ps != '\r') return false; b_newline = false; } ps++; } return false; } struct entry_meta_s { char *psz_name; char *psz_artist; char *psz_album_art; char *psz_mrl; char *psz_language; char *psz_tvgid; char *psz_grouptitle; vlc_tick_t i_duration; const char**ppsz_options; int i_options; vlc_meta_priority_t priority; }; static void entry_meta_Init( struct entry_meta_s *e ) { memset(e, 0, sizeof(*e)); e->i_duration = INPUT_DURATION_INDEFINITE; e->priority = VLC_META_PRIORITY_BASIC; } static void entry_meta_Clean( struct entry_meta_s *e ) { free( e->psz_name ); free( e->psz_artist ); free( e->psz_album_art ); free( e->psz_mrl ); free( e->psz_language ); free( e->psz_tvgid ); free( e->psz_grouptitle ); while( e->i_options-- ) free( (char*)e->ppsz_options[e->i_options] ); TAB_CLEAN( e->i_options, e->ppsz_options ); } static void parseEXTINF( char *, char *(*)(const char *), struct entry_meta_s * ); static int CreateEntry( input_item_node_t *p_node, const struct entry_meta_s *meta ) { if( !meta->psz_mrl ) return VLC_EGENERIC; input_item_t *p_input = input_item_NewExt( meta->psz_mrl, meta->psz_name, meta->i_duration, ITEM_TYPE_UNKNOWN, ITEM_NET_UNKNOWN ); if( !p_input ) return VLC_EGENERIC; input_item_AddOptions( p_input, meta->i_options, meta->ppsz_options, 0 ); vlc_mutex_lock( &p_input->lock ); if( meta->psz_artist ) vlc_meta_SetWithPriority( p_input->p_meta, vlc_meta_Artist, meta->psz_artist, meta->priority ); if( meta->psz_name ) vlc_meta_SetWithPriority( p_input->p_meta, vlc_meta_Title, meta->psz_name, meta->priority ); if( meta->psz_album_art ) vlc_meta_SetWithPriority( p_input->p_meta, vlc_meta_ArtworkURL, meta->psz_album_art, meta->priority ); if( meta->psz_language ) vlc_meta_SetWithPriority( p_input->p_meta, vlc_meta_Language, meta->psz_language, meta->priority ); if( meta->psz_grouptitle ) vlc_meta_SetWithPriority( p_input->p_meta, vlc_meta_Publisher, meta->psz_grouptitle, meta->priority ); vlc_mutex_unlock( &p_input->lock ); if( meta->psz_tvgid ) input_item_AddInfo( p_input, "XMLTV", "tvg-id", "%s", meta->psz_tvgid ); input_item_node_AppendItem( p_node, p_input ); input_item_Release( p_input ); return VLC_SUCCESS; } static int ReadDir( stream_t *p_demux, input_item_node_t *p_subitems ) { char *psz_line; char *psz_group = NULL; /* group is toggling tag */ struct entry_meta_s meta; entry_meta_Init( &meta ); char * (*pf_dup) (const char *) = p_demux->p_sys; psz_line = vlc_stream_ReadLine( p_demux->s ); while( psz_line ) { char *psz_parse = psz_line; /* Skip leading tabs and spaces */ while( *psz_parse == ' ' || *psz_parse == '\t' || *psz_parse == '\n' || *psz_parse == '\r' ) psz_parse++; if( *psz_parse == '#' ) { /* Parse extra info */ /* Skip leading tabs and spaces */ while( *psz_parse == ' ' || *psz_parse == '\t' || *psz_parse == '\n' || *psz_parse == '\r' || *psz_parse == '#' ) psz_parse++; if( !*psz_parse ) goto nextline; if( !strncasecmp( psz_parse, "EXTINF:", sizeof("EXTINF:") -1 ) ) { /* Extended info */ psz_parse += sizeof("EXTINF:") - 1; meta.i_duration = INPUT_DURATION_INDEFINITE; parseEXTINF( psz_parse, pf_dup, &meta ); meta.priority = VLC_META_PRIORITY_PLAYLIST; } else if( !strncasecmp( psz_parse, "EXTGRP:", sizeof("EXTGRP:") -1 ) ) { psz_parse += sizeof("EXTGRP:") - 1; if( *psz_parse ) { free( psz_group ); psz_group = pf_dup( psz_parse ); } } else if( !strncasecmp( psz_parse, "EXTVLCOPT:", sizeof("EXTVLCOPT:") -1 ) ) { /* VLC Option */ char *psz_option; psz_parse += sizeof("EXTVLCOPT:") -1; if( !*psz_parse ) goto nextline; psz_option = pf_dup( psz_parse ); if( psz_option ) TAB_APPEND( meta.i_options, meta.ppsz_options, psz_option ); } /* Special case for jamendo which provide the albumart */ else if( !strncasecmp( psz_parse, "EXTALBUMARTURL:", sizeof( "EXTALBUMARTURL:" ) -1 ) ) { psz_parse += sizeof( "EXTALBUMARTURL:" ) - 1; if( *psz_parse ) { free( meta.psz_album_art ); meta.psz_album_art = pf_dup( psz_parse ); meta.priority = VLC_META_PRIORITY_PLAYLIST; } } else if ( !strncasecmp( psz_parse, "PLAYLIST:", sizeof( "PLAYLIST:" ) - 1 ) ) { psz_parse += sizeof( "PLAYLIST:" ) - 1; input_item_SetTitle( p_subitems->p_item, psz_parse ); } } else if( !strncasecmp( psz_parse, "RTSPtext", sizeof("RTSPtext") -1 ) ) { ;/* special case to handle QuickTime RTSPtext redirect files */ } else if( *psz_parse ) { psz_parse = pf_dup( psz_parse ); if( !meta.psz_name && psz_parse ) /* Use filename as name for relative entries */ meta.psz_name = strdup( psz_parse ); if( psz_group && !meta.psz_grouptitle ) meta.psz_grouptitle = strdup( psz_group ); meta.psz_mrl = ProcessMRL( psz_parse, p_demux->psz_url ); free( psz_parse ); CreateEntry( p_subitems, &meta ); /* Cleanup state after entry */ entry_meta_Clean( &meta ); entry_meta_Init( &meta ); } nextline: /* Fetch another line */ free( psz_line ); psz_line = vlc_stream_ReadLine( p_demux->s ); if( !psz_line ) { /* Cleanup state */ entry_meta_Clean( &meta ); entry_meta_Init( &meta ); free( psz_group ); } } return VLC_SUCCESS; /* Needed for correct operation of go back */ } static void parseEXTINFTitle( char *psz_string, char *(*pf_dup) (const char *), struct entry_meta_s *meta ) { char *psz_item = strstr( psz_string, " - " ); /* here we have the 0.8.2+ format with artist */ if ( psz_item ) { /* *** "EXTINF:time,artist - name" */ *psz_item = '\0'; if( *psz_string ) meta->psz_artist = pf_dup( psz_string ); if( psz_item[3] ) meta->psz_name = pf_dup( &psz_item[3] ); /* points directly after ' - ' */ return; } /* reaching this point means: 0.8.1- with artist or something without artist */ if ( *psz_string == ',' ) { /* *** "EXTINF:time,,name" */ psz_string++; if( *psz_string ) meta->psz_name = pf_dup( psz_string ); return; } psz_item = psz_string; psz_string = strchr( psz_string, ',' ); if ( psz_string ) { /* *** "EXTINF:time,artist,name" */ *psz_string = '\0'; if( *psz_item ) meta->psz_artist = pf_dup( psz_item ); if( psz_string[1] ) meta->psz_name = pf_dup( &psz_string[1] ); } else { /* *** "EXTINF:time,name" */ if( *psz_item ) meta->psz_name = pf_dup( psz_item ); } } static void parseEXTINFIptvDiots( char *psz_string, char *(*pf_dup)(const char *), struct entry_meta_s *meta ) { char *psz = strchr( psz_string, '=' ); if( unlikely(!psz) ) return; char **ppsz_meta = NULL; *psz = 0; if( !strncasecmp( psz_string, "tvg-", 4 ) ) { if( !strcasecmp( psz_string + 4, "logo" ) ) ppsz_meta = &meta->psz_album_art; else if( !strcasecmp( psz_string + 4, "name" ) ) ppsz_meta = &meta->psz_name; else if( !strcasecmp( psz_string + 4, "language" ) ) ppsz_meta = &meta->psz_language; else if( !strcasecmp( psz_string + 4, "id" ) ) ppsz_meta = &meta->psz_tvgid; } else if( !strcasecmp( psz_string, "group-title" ) ) { ppsz_meta = &meta->psz_grouptitle; } *psz = '='; if( !ppsz_meta || *ppsz_meta /* no overwrite */ ) return; size_t i_valuelen = strlen( ++psz ); if( unlikely(i_valuelen == 0) ) return; bool b_escaped = (*psz == '"'); if( i_valuelen > 2 && b_escaped ) { psz[ i_valuelen - 1 ] = 0; *ppsz_meta = pf_dup( psz + 1 ); } else *ppsz_meta = pf_dup( psz ); } static void parseEXTINFIptvDiotsInDuration( char *psz_string, char *(*pf_dup)(const char *), struct entry_meta_s *meta, char **ppsz_end ) { for( ;; ) { while( *psz_string == '\t' || *psz_string == ' ' ) psz_string++; char *psz_start = psz_string; bool b_escaped = false; bool b_key = false; bool b_value = false; bool b_next = false; for( ;!b_next; ++psz_string ) { switch( *psz_string ) { case ',': /* Last comma for title */ if(!b_escaped) { if(b_value) { *psz_string = '\0'; parseEXTINFIptvDiots( psz_start, pf_dup, meta ); } *ppsz_end = psz_string + 1; return; } break; case '"': if(!b_escaped && b_value) return; if(!b_key) return; b_value = true; b_escaped = !b_escaped; break; case '\0': if(!b_escaped && b_value) parseEXTINFIptvDiots( psz_start, pf_dup, meta ); return; case ' ': if(!b_escaped) { *psz_string = '\0'; if(b_value) parseEXTINFIptvDiots( psz_start, pf_dup, meta ); b_next = true; } break; case '=': b_key = true; break; default: if(b_key) b_value = true; break; } } } } static void parseEXTINF( char *psz_string, char *(*pf_dup)(const char *), struct entry_meta_s *meta ) { char *end = psz_string + strlen( psz_string ); FREENULL( meta->psz_name ); FREENULL( meta->psz_artist ); /* strip leading whitespaces */ while( psz_string < end && ( *psz_string == '\t' || *psz_string == ' ' ) ) psz_string++; /* Parse duration */ char *psz_end = NULL; float i_parsed_duration = vlc_strtof_c( psz_string, &psz_end ); if( i_parsed_duration > 0 ) meta->i_duration = vlc_tick_from_sec( (double)i_parsed_duration ); /* skip to first unmatched */ if( psz_end ) psz_string = psz_end; /* skip whitespaces */ while( psz_string < end && ( *psz_string == '\t' || *psz_string == ' ' ) ) psz_string++; if( psz_string == end ) return; /* EXTINF:1,title*/ /* EXTINF: -123.12 ,title*/ if( *psz_string == ',' ) { if( ++psz_string < end ) parseEXTINFTitle( psz_string, pf_dup, meta ); } /* EXTINF: -1 tvg-foo="val" tvg-foo2="val",title */ /* EXTINF: -1 tvg-foo="val,val2" ,title */ else if( *psz_string >= 'A' && *psz_string <= 'z' ) { psz_end = NULL; parseEXTINFIptvDiotsInDuration( psz_string, pf_dup, meta, &psz_end ); if( psz_end && *psz_end ) /* returns past last comma */ { free(meta->psz_name); meta->psz_name = pf_dup( psz_end ); } } }