From 6b3fa55cc61403201cdc471e62a608f1b93aa1a4 Mon Sep 17 00:00:00 2001 From: Rmys Date: Sat, 20 May 2023 16:00:27 +0300 Subject: [PATCH] vlc rebuild --- multimedia/videoplayer/vlc/files/youtube.lua | 1115 ++++++++++++++++++ multimedia/videoplayer/vlc/pspec.xml | 10 + 2 files changed, 1125 insertions(+) create mode 100644 multimedia/videoplayer/vlc/files/youtube.lua diff --git a/multimedia/videoplayer/vlc/files/youtube.lua b/multimedia/videoplayer/vlc/files/youtube.lua new file mode 100644 index 0000000000..549cee4dbf --- /dev/null +++ b/multimedia/videoplayer/vlc/files/youtube.lua @@ -0,0 +1,1115 @@ +--[[ + + 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, '