--[[ $Id$ Copyright © 2007-2019 the VideoLAN team 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. --]] -- Helper function to get a parameter's value in a URL function get_url_param( url, name ) local _, _, res = string.find( url, "[&?]"..name.."=([^&]*)" ) return res end -- Helper function to copy a parameter when building a new URL function copy_url_param( url, name ) local value = get_url_param( url, name ) return ( value and "&"..name.."="..value or "" ) -- Ternary operator end function get_arturl() local iurl = get_url_param( vlc.path, "iurl" ) if iurl then return iurl end local video_id = get_url_param( vlc.path, "v" ) if not video_id then return nil end return vlc.access.."://img.youtube.com/vi/"..video_id.."/default.jpg" end -- Pick the most suited format available function get_fmt( fmt_list ) local prefres = vlc.var.inherit(nil, "preferred-resolution") if prefres < 0 then return nil end local fmt = nil for itag,height in string.gmatch( fmt_list, "(%d+)/%d+x(%d+)[^,]*" ) do -- Apparently formats are listed in quality -- order, so we take the first one that works, -- or fallback to the lowest quality fmt = itag if tonumber(height) <= prefres then break end end return fmt end -- Buffering iterator to parse through the HTTP stream several times -- without making several HTTP requests function buf_iter( s ) s.i = s.i + 1 local line = s.lines[s.i] if not line then -- Put back together statements split across several lines, -- otherwise we won't be able to parse them repeat local l = s.stream:readline() if not l then break end line = line and line..l or l -- Ternary operator until string.match( line, "};$" ) if line then s.lines[s.i] = line end end return line end -- Helper to search and extract code from javascript stream function js_extract( js, pattern ) js.i = 0 -- Reset to beginning for line in buf_iter, js do local ex = string.match( line, pattern ) if ex then return ex end end vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) return nil end -- Descramble the URL signature using the javascript code that does that -- in the web page function js_descramble( sig, js_url ) -- Fetch javascript code local js = { stream = vlc.stream( js_url ), lines = {}, i = 0 } if not js.stream then vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) return sig end -- Look for the descrambler function's name -- k.s&&f.set(k.sp,encodeURIComponent(DK(decodeURIComponent(k.s)))); -- k.s (from stream map field "s") holds the input scrambled signature -- k.sp (from stream map field "sp") holds a parameter name (normally -- "signature" or "sig") to set with the output, descrambled signature local descrambler = js_extract( js, "%.set%([^,]-%.sp,[^;]-%((..)%(" ) if not descrambler then vlc.msg.dbg( "Couldn't extract youtube video URL signature descrambling function name" ) return sig end -- Fetch the code of the descrambler function -- Go=function(a){a=a.split("");Fo.sH(a,2);Fo.TU(a,28);Fo.TU(a,44);Fo.TU(a,26);Fo.TU(a,40);Fo.TU(a,64);Fo.TR(a,26);Fo.sH(a,1);return a.join("")}; local rules = js_extract( js, "^"..descrambler.."=function%([^)]*%){(.-)};" ) if not rules then vlc.msg.dbg( "Couldn't extract youtube video URL signature descrambling rules" ) return sig end -- Get the name of the helper object providing transformation definitions local helper = string.match( rules, ";(..)%...%(" ) if not helper then vlc.msg.dbg( "Couldn't extract youtube video URL signature transformation helper name" ) vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) return sig end -- Fetch the helper object code -- var Fo={TR:function(a){a.reverse()},TU:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c},sH:function(a,b){a.splice(0,b)}}; local transformations = js_extract( js, "[ ,]"..helper.."={(.-)};" ) if not transformations then vlc.msg.dbg( "Couldn't extract youtube video URL signature transformation code" ) return sig end -- Parse the helper object to map available transformations local trans = {} for meth,code in string.gmatch( transformations, "(..):function%([^)]*%){([^}]*)}" ) do -- a=a.reverse() if string.match( code, "%.reverse%(" ) then trans[meth] = "reverse" -- a.splice(0,b) elseif string.match( code, "%.splice%(") then trans[meth] = "slice" -- var c=a[0];a[0]=a[b%a.length];a[b]=c elseif string.match( code, "var c=" ) then trans[meth] = "swap" else vlc.msg.warn("Couldn't parse unknown youtube video URL signature transformation") end end -- Parse descrambling rules, map them to known transformations -- and apply them on the signature local missing = false for meth,idx in string.gmatch( rules, "..%.(..)%([^,]+,(%d+)%)" ) do idx = tonumber( idx ) if trans[meth] == "reverse" then sig = string.reverse( sig ) elseif trans[meth] == "slice" then sig = string.sub( sig, idx + 1 ) elseif trans[meth] == "swap" then if idx > 1 then sig = string.gsub( sig, "^(.)("..string.rep( ".", idx - 1 )..")(.)(.*)$", "%3%2%1%4" ) elseif idx == 1 then sig = string.gsub( sig, "^(.)(.)", "%2%1" ) end else vlc.msg.dbg("Couldn't apply unknown youtube video URL signature transformation") missing = true end end if missing then vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) end return sig end -- Parse and assemble video stream URL function stream_url( params, js_url ) local url = string.match( params, "url=([^&]+)" ) if not url then return nil end url = vlc.strings.decode_uri( url ) -- Descramble any scrambled signature and append it to URL local s = string.match( params, "s=([^&]+)" ) if s then s = vlc.strings.decode_uri( s ) vlc.msg.dbg( "Found "..string.len( s ).."-character scrambled signature for youtube video URL, attempting to descramble... " ) if js_url then s = js_descramble( s, js_url ) else vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) end local sp = string.match( params, "sp=([^&]+)" ) if not sp then vlc.msg.warn( "Couldn't extract signature parameters for youtube video URL, guessing" ) sp = "signature" end url = url.."&"..sp.."="..vlc.strings.encode_uri_component( s ) end return url end -- Parse and pick our video stream URL (classic parameters) function pick_url( url_map, fmt, js_url ) for stream in string.gmatch( url_map, "[^,]+" ) do local itag = string.match( stream, "itag=(%d+)" ) if not fmt or not itag or tonumber( itag ) == tonumber( fmt ) then return stream_url( stream, js_url ) end end return nil end -- Parse and pick our video stream URL (new-style parameters) function pick_stream( stream_map, js_url ) local pick = nil local fmt = tonumber( get_url_param( vlc.path, "fmt" ) ) if fmt then -- Legacy match from URL parameter for stream in string.gmatch( stream_map, '{(.-)}' ) do local itag = tonumber( string.match( stream, '"itag":(%d+)' ) ) if fmt == itag then pick = stream break end end else -- Compare the different available formats listed with our -- quality targets local prefres = vlc.var.inherit( nil, "preferred-resolution" ) local bestres = nil for stream in string.gmatch( stream_map, '{(.-)}' ) do local height = tonumber( string.match( stream, '"height":(%d+)' ) ) -- Better than nothing if not pick or ( height and ( not bestres -- Better quality within limits or ( ( prefres < 0 or height <= prefres ) and height > bestres ) -- Lower quality more suited to limits or ( prefres > -1 and bestres > prefres and height < bestres ) ) ) then bestres = height pick = stream end end end if not pick then return nil end -- Either the "url" or the "cipher" parameter is present, -- depending on whether the URL signature is scrambled. local cipher = string.match( pick, '"cipher":"(.-)"' ) if cipher then -- Scrambled signature: some assembly required local url = stream_url( cipher, js_url ) if url then return url end end -- Unscrambled signature, already included in ready-to-use URL return string.match( pick, '"url":"(.-)"' ) end -- Probe function. function probe() return ( ( vlc.access == "http" or vlc.access == "https" ) and ( string.match( vlc.path, "^www%.youtube%.com/" ) or string.match( vlc.path, "^gaming%.youtube%.com/" ) ) and ( string.match( vlc.path, "/watch%?" ) -- the html page or string.match( vlc.path, "/live$" ) -- user live stream html page or string.match( vlc.path, "/live%?" ) -- user live stream html page or string.match( vlc.path, "/get_video_info%?" ) -- info API or string.match( vlc.path, "/v/" ) -- video in swf player or string.match( vlc.path, "/embed/" ) -- embedded player iframe ) ) end -- Parse function. function parse() if string.match( vlc.path, "^gaming%.youtube%.com/" ) then url = string.gsub( vlc.path, "^gaming%.youtube%.com", "www.youtube.com" ) return { { path = vlc.access.."://"..url } } end if string.match( vlc.path, "/watch%?" ) or string.match( vlc.path, "/live$" ) or string.match( vlc.path, "/live%?" ) then -- This is the HTML page's URL -- fmt is the format of the video -- (cf. http://en.wikipedia.org/wiki/YouTube#Quality_and_formats) fmt = get_url_param( vlc.path, "fmt" ) while true do -- Try to find the video's title line = vlc.readline() if not line then break end if string.match( line, "]*>(.-)

" ) if description then description = vlc.strings.resolve_xml_special_chars( description ) end end if string.match( line, "