--[[ Copyright © 2007-2023 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 -- Helper emulating vlc.readline() to work around its failure on -- very long lines (see #24957) function read_long_line() local eol local pos = 0 local len = 32768 repeat len = len * 2 local line = vlc.peek( len ) if not line then return nil end eol = string.find( line, "\n", pos + 1 ) pos = len until eol or len >= 1024 * 1024 -- No EOF detection, loop until limit return vlc.read( eol or len ) 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 return nil end -- Descramble the "n" parameter using the javascript code that does that -- in the web page function n_descramble( nparam, js ) if not js.stream then if not js.url then return nil end js.stream = vlc.stream( js_url ) if not js.stream then -- Retry once for transient errors js.stream = vlc.stream( js_url ) if not js.stream then return nil end end end -- Look for the descrambler function's name -- a.C&&(b=a.get("n"))&&(b=Bpa[0](b),a.set("n",b),Bpa.length||iha(""))}}; -- var Bpa=[iha]; local callsite = js_extract( js, '[^;]*%.set%("n",[^};]*' ) if not callsite then vlc.msg.dbg( "Couldn't extract YouTube video throttling parameter descrambling function name" ) return nil end -- Try direct function name from following clause local descrambler = string.match( callsite, '%.set%("n",.%),...?%.length||(...?)%(' ) local itm = nil if not descrambler then -- Try from main call site descrambler = string.match( callsite, '[=%(,&|]([a-zA-Z0-9_$%[%]]+)%(.%),.%.set%("n",' ) if descrambler then -- Check if this is only an intermediate variable itm = string.match( descrambler, '^([^%[%]]+)%[' ) if itm then descrambler = nil end else -- Last chance: intermediate variable in following clause itm = string.match( callsite, '%.set%("n",.%),(...?)%.length' ) end end if not descrambler and itm then -- Resolve intermediate variable descrambler = js_extract( js, 'var '..itm..'=%[(...?)[%],]' ) end if not descrambler then vlc.msg.dbg( "Couldn't extract YouTube video throttling parameter descrambling function name" ) return nil end -- Fetch the code of the descrambler function -- lha=function(a){var b=a.split(""),c=[310282131,"KLf3",b,null,function(d,e){d.push(e)},-45817231, [data and transformations...] ,1248130556];c[3]=c;c[15]=c;c[18]=c;try{c[40](c[14],c[2]),c[25](c[48]),c[21](c[32],c[23]), [scripted calls...] ,c[25](c[33],c[3])}catch(d){return"enhanced_except_4ZMBnuz-_w8_"+a}return b.join("")}; local code = js_extract( js, "^"..descrambler.."=function%([^)]*%){(.-)};" ) if not code then vlc.msg.dbg( "Couldn't extract YouTube video throttling parameter descrambling code" ) return nil end -- Split code into two main sections: 1/ data and transformations, -- and 2/ a script of calls local datac, script = string.match( code, "c=%[(.*)%];.-;try{(.*)}catch%(" ) if ( not datac ) or ( not script ) then vlc.msg.dbg( "Couldn't extract YouTube video throttling parameter descrambling rules" ) return nil end -- Split "n" parameter into a table as descrambling operates on it -- as one of several arrays local n = {} for c in string.gmatch( nparam, "." ) do table.insert( n, c ) end -- Helper local table_len = function( tab ) local len = 0 for i, val in ipairs( tab ) do len = len + 1 end return len end -- Shared core section of compound transformations: it compounds -- the "n" parameter with an input string, character by character, -- using a Base64 alphabet as algebraic modulo group. -- var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split("")) local compound = function( ntab, str, alphabet ) if ntab ~= n or type( str ) ~= "string" or type( alphabet ) ~= "string" then return true end local input = {} for c in string.gmatch( str, "." ) do table.insert( input, c ) end local len = string.len( alphabet ) for i, c in ipairs( ntab ) do if type( c ) ~= "string" then return true end local pos1 = string.find( alphabet, c, 1, true ) local pos2 = string.find( alphabet, input[i], 1, true ) if ( not pos1 ) or ( not pos2 ) then return true end local pos = ( pos1 - pos2 ) % len local newc = string.sub( alphabet, pos + 1, pos + 1 ) ntab[i] = newc table.insert( input, newc ) end end -- The data section contains among others function code for a number -- of transformations, most of which are basic array operations. -- We can match these functions' code to identify them, and emulate -- the corresponding transformations. local trans = { reverse = { func = function( tab ) local len = table_len( tab ) local tmp = {} for i, val in ipairs( tab ) do tmp[len - i + 1] = val end for i, val in ipairs( tmp ) do tab[i] = val end end, match = { -- function(d){d.reverse()} -- function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])} "^function%(d%)", } }, append = { func = function( tab, val ) table.insert( tab, val ) end, match = { -- function(d,e){d.push(e)} "^function%(d,e%){d%.push%(e%)},", } }, remove = { func = function( tab, i ) if type( i ) ~= "number" then return true end i = i % table_len( tab ) table.remove( tab, i + 1 ) end, match = { -- function(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)} "^[^}]-;d%.splice%(e,1%)},", } }, swap = { func = function( tab, i ) if type( i ) ~= "number" then return true end i = i % table_len( tab ) local tmp = tab[1] tab[1] = tab[i + 1] tab[i + 1] = tmp end, match = { -- function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f} -- function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])} "^[^}]-;var f=d%[0%];d%[0%]=d%[e%];d%[e%]=f},", "^[^}]-;d%.splice%(0,1,d%.splice%(e,1,d%[0%]%)%[0%]%)},", } }, rotate = { func = function( tab, shift ) if type( shift ) ~= "number" then return true end local len = table_len( tab ) shift = shift % len local tmp = {} for i, val in ipairs( tab ) do tmp[( i - 1 + shift ) % len + 1] = val end for i, val in ipairs( tmp ) do tab[i] = val end end, match = { -- function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())} -- function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})} "^[^}]-d%.unshift%(d.pop%(%)%)},", "^[^}]-d%.unshift%(f%)}%)},", } }, -- Here functions with no arguments are not really functions, -- they're constants: treat them as such. These alphabets are -- passed to and used by the compound transformations. alphabet1 = { func = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_", match = { -- function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e} "^function%(%){[^}]-case 58:d=96;", } }, alphabet2 = { func = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", match = { -- function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 58:d-=14;case 91:case 92:case 93:continue;case 123:d=47;case 94:case 95:case 96:continue;case 46:d=95}e.push(String.fromCharCode(d))}return e} -- function(){for(var d=64,e=[];++d-e.length-32;)switch(d){case 46:d=95;default:e.push(String.fromCharCode(d));case 94:case 95:case 96:break;case 123:d-=76;case 92:case 93:continue;case 58:d=44;case 91:}return e} "^function%(%){[^}]-case 58:d%-=14;", "^function%(%){[^}]-case 58:d=44;", } }, -- Compound transformations are based on a shared core section -- that compounds the "n" parameter with an input string, -- character by character, using a variation of a Base64 -- alphabet as algebraic modulo group. compound = { func = compound, match = { -- function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))} "^function%(d,e,f%)", } }, -- These compound transformation variants first build their -- Base64 alphabet themselves, before using it. compound1 = { func = function( ntab, str ) return compound( ntab, str, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" ) end, match = { -- function(d,e){for(var f=64,h=[];++f-h.length-32;)switch(f){case 58:f=96;continue;case 91:f=44;break;case 65:f=47;continue;case 46:f=153;case 123:f-=58;default:h.push(String.fromCharCode(f))} [ compound... ] } "^function%(d,e%){[^}]-case 58:f=96;", } }, compound2 = { func = function( ntab, str ) return compound( ntab, str,"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" ) end, match = { -- function(d,e){for(var f=64,h=[];++f-h.length-32;){switch(f){case 58:f-=14;case 91:case 92:case 93:continue;case 123:f=47;case 94:case 95:case 96:continue;case 46:f=95}h.push(String.fromCharCode(f))} [ compound... ] } -- function(d,e){for(var f=64,h=[];++f-h.length-32;)switch(f){case 46:f=95;default:h.push(String.fromCharCode(f));case 94:case 95:case 96:break;case 123:f-=76;case 92:case 93:continue;case 58:f=44;case 91:} [ compound... ] } "^function%(d,e%){[^}]-case 58:f%-=14;", "^function%(d,e%){[^}]-case 58:f=44;", } }, -- Fallback unid = { func = function( ) vlc.msg.dbg( "Couldn't apply unidentified YouTube video throttling parameter transformation, aborting descrambling" ) return true end, match = { } }, } -- The data section actually mixes input data, reference to the -- "n" parameter array, and self-reference to its own array, with -- transformation functions used to modify itself. We parse it -- as such into a table. local data = {} datac = datac.."," while datac and datac ~= "" do local el = nil -- Transformation functions if string.match( datac, "^function%(" ) then for name, tr in pairs( trans ) do for i, match in ipairs( tr.match ) do if string.match( datac, match ) then el = tr.func break end end if el then break end end if not el then el = trans.unid.func vlc.msg.warn( "Couldn't parse unidentified YouTube video throttling parameter transformation" ) end -- Compounding functions use a subfunction, so we need to be -- more specific in how much parsed data we consume. if el == trans.compound.func or el == trans.compound1.func or el == trans.compound2.func then datac = string.match( datac, '^.-},e%.split%(""%)%)},(.*)$' ) or string.match( datac, "^.-},(.*)$" ) else datac = string.match( datac, "^.-},(.*)$" ) end -- String input data elseif string.match( datac, '^"[^"]*",' ) then el, datac = string.match( datac, '^"([^"]*)",(.*)$' ) -- Integer input data -- 1818016376,-648890305,-1200559E3, ... elseif string.match( datac, '^%-?%d+,' ) or string.match( datac, '^%-?%d+[eE]%-?%d+,' ) then el, datac = string.match( datac, "^(.-),(.*)$" ) el = tonumber( el ) -- Reference to "n" parameter array elseif string.match( datac, '^b,' ) then el = n datac = string.match( datac, "^b,(.*)$" ) -- Replaced by self-reference to data array after its declaration elseif string.match( datac, '^null,' ) then el = data datac = string.match( datac, "^null,(.*)$" ) else vlc.msg.warn( "Couldn't parse unidentified YouTube video throttling parameter descrambling data" ) el = false -- Lua tables can't contain nil values datac = string.match( datac, "^[^,]-,(.*)$" ) end table.insert( data, el ) end -- Debugging helper to print data array elements local prd = function( el, tab ) if not el then return "???" elseif el == n then return "n" elseif el == data then return "data" elseif type( el ) == "string" then return '"'..el..'"' elseif type( el ) == "number" then el = tostring( el ) if type( tab ) == "table" then el = el.." -> "..( el % table_len( tab ) ) end return el else for name, tr in pairs( trans ) do if el == tr.func then return name end end return tostring( el ) end end -- The script section contains a series of calls to elements of -- the data section array onto other elements of it: calls to -- transformations, with a reference to the data array itself or -- the "n" parameter array as first argument, and often input data -- as a second argument. We parse and emulate those calls to follow -- the descrambling script. -- c[40](c[14],c[2]),c[25](c[48]),c[14](c[1],c[24],c[42]()), [...] if not string.match( script, "c%[(%d+)%]%(c%[(%d+)%]([^)]-)%)" ) then vlc.msg.dbg( "Couldn't parse and execute YouTube video throttling parameter descrambling rules" ) return nil end for ifunc, itab, args in string.gmatch( script, "c%[(%d+)%]%(c%[(%d+)%]([^)]-)%)" ) do local iarg1 = string.match( args, "^,c%[(%d+)%]" ) local iarg2 = string.match( args, "^,[^,]-,c%[(%d+)%]" ) local func = data[tonumber( ifunc ) + 1] local tab = data[tonumber( itab ) + 1] local arg1 = iarg1 and data[tonumber( iarg1 ) + 1] local arg2 = iarg2 and data[tonumber( iarg2 ) + 1] -- Uncomment to debug transformation chain --vlc.msg.err( '"n" parameter transformation: '..prd( func ).."("..prd( tab )..( arg1 ~= nil and ( ", "..prd( arg1, tab ) ) or "" )..( arg2 ~= nil and ( ", "..prd( arg2, tab ) ) or "" )..") "..ifunc.."("..itab..( iarg1 and ( ", "..iarg1 ) or "" )..( iarg2 and ( ", "..iarg2 ) or "" )..")" ) --local nprev = table.concat( n ) if type( func ) ~= "function" or type( tab ) ~= "table" or func( tab, arg1, arg2 ) then vlc.msg.dbg( "Invalid data type encountered during YouTube video throttling parameter descrambling transformation chain, aborting" ) vlc.msg.dbg( "Couldn't descramble YouTube throttling URL parameter: data transfer will get throttled" ) vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) break end -- Uncomment to debug transformation chain --local nnew = table.concat( n ) --if nprev ~= nnew then -- vlc.msg.dbg( '"n" parameter transformation: '..nprev.." -> "..nnew ) --end end return table.concat( n ) end -- Descramble the URL signature using the javascript code that does that -- in the web page function sig_descramble( sig, js ) if not js.stream then if not js.url then return nil end js.stream = vlc.stream( js.url ) if not js.stream then -- Retry once for transient errors js.stream = vlc.stream( js.url ) if not js.stream then return nil end end end -- Look for the descrambler function's name -- if(h.s){var l=h.sp,m=wja(decodeURIComponent(h.s));f.set(l,encodeURIComponent(m))} -- 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, "[=%(,&|](...?)%(decodeURIComponent%(.%.s%)%)" ) if not descrambler then vlc.msg.dbg( "Couldn't extract youtube video URL signature descrambling function name" ) return nil 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 nil 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" ) return nil 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 nil 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 ) 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... " ) local ds = sig_descramble( s, js ) if not ds then vlc.msg.dbg( "Couldn't descramble YouTube video URL signature" ) vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) ds = s 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( ds ) end return url end -- Parse and pick our video stream URL (classic parameters, out of use) 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 nil -- stream_url( stream, js_url ) end end return nil end -- Pick suitable stream among available formats function pick_stream( formats, fmt ) if not formats then return nil end -- Remove subobject fields to ease parsing of stream object array formats = string.gsub( formats, '"[^"]-":{[^{}]-},?', '' ) if tonumber( fmt ) then -- Legacy match from URL parameter fmt = tonumber( fmt ) for stream in string.gmatch( formats, '{(.-)}' ) do local itag = tonumber( string.match( stream, '"itag":(%d+)' ) ) if fmt == itag then return stream end end return nil else -- Compare the different available formats listed with our -- quality targets local prefres = vlc.var.inherit( nil, "preferred-resolution" ) local bestres, pick for stream in string.gmatch( formats, '{(.-)}' ) do local height = tonumber( string.match( stream, '"height":(%d+)' ) ) -- We have no preference mechanism for audio formats, -- so just pick the first one if fmt == "audio" and not height then return stream end -- Better than nothing if ( not pick and fmt ~= "video" ) 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 return pick end end -- Parse and pick our video stream URL (new-style parameters) function pick_stream_url( muxed, adaptive, js_url, fmt ) -- Shared JavaScript resources - lazy initialization local js = { url = js_url, stream = nil, lines = {}, i = 0 } if not js.url then vlc.msg.warn( "Couldn't extract YouTube JavaScript player code URL, descrambling functions unavailable" ) end local pick = nil if tonumber( fmt ) then -- Specific numeric itag, search in both lists pick = pick_stream( muxed, fmt ) if not pick then pick = pick_stream( adaptive, fmt ) end elseif ( fmt == "audio" or fmt == "video" ) then -- Specifically audio or video only, no fallback pick = pick_stream( adaptive, fmt ) else if fmt == "hd" then -- Try and leverage full array of adaptive formats local audio = pick_stream( adaptive, "audio" ) local video = pick_stream( adaptive, "video" ) if audio and video then local audio_url = assemble_stream_url( audio, js ) local video_url = assemble_stream_url( video, js ) if audio_url and video_url then return video_url, audio_url end end end if not pick then -- Default or fallback: safe old multiplexed streams, -- but reduced to a single, low-definition format -- available in some cases pick = pick_stream( muxed, fmt ) end end if not pick then return nil end return assemble_stream_url( pick, js ) end -- Parse, descramble and assemble elements of video stream URL function assemble_stream_url( pick, js ) -- 1/ URL signature -- Either the "url" or the "signatureCipher" parameter is present, -- depending on whether the URL signature is scrambled. local url local cipher = string.match( pick, '"signatureCipher":"(.-)"' ) or string.match( pick, '"[a-zA-Z]*[Cc]ipher":"(.-)"' ) if cipher then -- Scrambled signature: some assembly required url = stream_url( cipher, js ) end if not url then -- Unscrambled signature, already included in ready-to-use URL url = string.match( pick, '"url":"(.-)"' ) end if not url then return nil end -- 2/ Data transfer throttling -- The "n" parameter is scrambled too, and needs to be descrambled -- and replaced in place, otherwise the data transfer gets throttled -- down to between 40 and 80 kB/s, below real-time playability level. local n = string.match( url, "[?&]n=([^&]+)" ) if n then n = vlc.strings.decode_uri( n ) local dn = nil -- n_descramble( n, js ) if dn then url = string.gsub( url, "([?&])n=[^&]+", "%1n="..vlc.strings.encode_uri_component( dn ), 1 ) else vlc.msg.err( "Couldn't descramble YouTube throttling URL parameter: data transfer will get throttled" ) --vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" ) end end return 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, "^music%.youtube%.com/" ) or string.match( vlc.path, "^gaming%.youtube%.com/" ) -- out of use ) 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, "/shorts/" ) -- YouTube Shorts 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 )) or string.match( vlc.path, "^consent%.youtube%.com/" ) ) ) end -- Parse function. function parse() if string.match( vlc.path, "^consent%.youtube%.com/" ) then -- Cookie consent redirection -- Location: https://consent.youtube.com/m?continue=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXXXXXXXXXXX&gl=FR&m=0&pc=yt&uxe=23983172&hl=fr&src=1 -- Set-Cookie: CONSENT=PENDING+355; expires=Fri, 01-Jan-2038 00:00:00 GMT; path=/; domain=.youtube.com local url = get_url_param( vlc.path, "continue" ) if not url then vlc.msg.err( "Couldn't handle YouTube cookie consent redirection, please check for updates to this script or try disabling HTTP cookie forwarding" ) return { } end return { { path = vlc.strings.decode_uri( url ), options = { ":no-http-forward-cookies" } } } elseif not string.match( vlc.path, "^www%.youtube%.com/" ) then -- Skin subdomain return { { path = vlc.access.."://"..string.gsub( vlc.path, "^([^/]*)/", "www.youtube.com/" ) } } elseif string.match( vlc.path, "/watch%?" ) or string.match( vlc.path, "/live$" ) or string.match( vlc.path, "/live%?" ) or string.match( vlc.path, "/shorts/" ) then -- This is the HTML page's URL local path, path2, title, description, artist, arturl, js_url -- Retired YouTube API for video format itag parameter, -- still supported and extended as youtube.lua API -- https://en.wikipedia.org/w/index.php?title=YouTube&oldid=716878321#Quality_and_formats local fmt = get_url_param( vlc.path, "fmt" ) while true do -- The new HTML code layout has fewer and longer lines; always -- use the long line workaround until we get more visibility. local line = new_layout and read_long_line() or vlc.readline() if not line then break end -- The next line is the major configuration line that we need. -- It is very long so we need this workaround (see #24957). if string.match( line, '^ *
' ) then line = read_long_line() if not line then break end end if not title then local meta = string.match( line, '' ) if meta then title = string.match( meta, ' content="(.-)"' ) if title then title = vlc.strings.resolve_xml_special_chars( title ) end end end if not description then -- FIXME: there is another version of this available, -- without the double JSON string encoding, but we're -- unlikely to access it due to #24957 description = string.match( line, '\\"shortDescription\\":\\"(.-[^\\])\\"') if description then -- FIXME: do this properly (see #24958) description = string.gsub( description, '\\(["\\/])', '%1' ) else description = string.match( line, '"shortDescription":"(.-[^\\])"') end if description then if string.match( description, '^"' ) then description = "" end -- FIXME: do this properly (see #24958) -- This way of unescaping is technically wrong -- so as little as possible of it should be done description = string.gsub( description, '\\(["\\/])', '%1' ) description = string.gsub( description, '\\n', '\n' ) description = string.gsub( description, '\\r', '\r' ) description = string.gsub( description, "\\u0026", "&" ) end end if not arturl then local meta = string.match( line, '' ) if meta then arturl = string.match( meta, ' content="(.-)"' ) if arturl then arturl = vlc.strings.resolve_xml_special_chars( arturl ) end end end if not artist then artist = string.match(line, '\\"author\\":\\"(.-)\\"') if artist then -- FIXME: do this properly (see #24958) artist = string.gsub( artist, '\\(["\\/])', '%1' ) else artist = string.match( line, '"author":"(.-)"' ) end if artist then -- FIXME: do this properly (see #24958) artist = string.gsub( artist, "\\u0026", "&" ) end end if not new_layout then if string.match( line, '