diff --git a/share/lua/playlist/youtube.lua b/share/lua/playlist/youtube.lua index 4c45d0ead1..fb28c7943e 100644 --- a/share/lua/playlist/youtube.lua +++ b/share/lua/playlist/youtube.lua @@ -111,6 +111,320 @@ function js_extract( js, pattern ) 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 then + return nil + end + + -- Look for the descrambler function's name + -- a.D&&(b=a.get("n"))&&(b=lha(b),a.set("n",b))}}; + local descrambler = js_extract( js, '[=%(,&|](...?)%(.%),.%.set%("n",' ) + 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 + + -- Common routine shared by the compound transformations, + -- compounding the "n" parameter with an input string, + -- character by character using a Base64 alphabet. + -- d.forEach(function(l,m,n){this.push(n[m]=h[(h.indexOf(l)-h.indexOf(this[m])+m-32+f--)%h.length])},e.split("")) + local compound = function( ntab, str, alphabet, charcode ) + if ntab ~= n or type( str ) ~= "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 + charcode - 32 ) % 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%)}%)},", + } + }, + -- Compound transformations first build a variation of a + -- Base64 alphabet, then in a common section, compound the + -- "n" parameter with an input string, character by character. + compound1 = { + func = function( ntab, str ) + return compound( ntab, str, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_", 96 ) + 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... ] } + "^[^}]-case 58:f=96;", + } + }, + compound2 = { + func = function( ntab, str ) + return compound( ntab, str,"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", 96 ) + 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... ] } + "^[^}]-case 58:f%-=14;", + "^[^}]-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 ~= "" 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.compound1.func or el == trans.compound2.func then + datac = string.match( datac, '^.-},e%.split%(""%)%)},(.*)$' ) + else + datac = string.match( datac, "^.-},(.*)$" ) + end + + -- String input data + elseif string.match( datac, '^"[^"]*",' ) then + el, datac = string.match( datac, '^"([^"]*)",(.*)$' ) + -- Integer input data + elseif string.match( datac, '^-?%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[21](c[32],c[23]), [...] + for ifunc, itab, iarg in string.gmatch( script, "c%[(%d+)%]%(c%[(%d+)%]([^)]-)%)" ) do + iarg = string.match( iarg, ",c%[(%d+)%]" ) + + local func = data[tonumber( ifunc ) + 1] + local tab = data[tonumber( itab ) + 1] + local arg = iarg and data[tonumber( iarg ) + 1] + + -- Uncomment to debug transformation chain + --vlc.msg.dbg( '"n" parameter transformation: '..prd( func ).."("..prd( tab )..( arg ~= nil and ( ", "..prd( arg, tab ) ) or "" )..") "..ifunc.."("..itab..( iarg and ( ", "..iarg ) or "" )..")" ) + --local nprev = table.concat( n ) + + if type( func ) ~= "function" or type( tab ) ~= "table" + or func( tab, arg ) 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 ) @@ -298,17 +612,38 @@ function pick_stream( stream_map, js_url ) -- 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 - local url = stream_url( cipher, js ) - if url then - return url + 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 + + -- 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 = n_descramble( n, js ) + if dn then + url = string.gsub( url, "([?&])n=[^&]+", "%1n="..vlc.strings.encode_uri_component( dn ), 1 ) + else + 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" ) end end - -- Unscrambled signature, already included in ready-to-use URL - return string.match( pick, '"url":"(.-)"' ) + + return url end -- Probe function.