diff --git a/MyScreenSaver/Properties/AssemblyInfo.cs b/MyScreenSaver/Properties/AssemblyInfo.cs
index 49e4a58..7d1a74f 100644
--- a/MyScreenSaver/Properties/AssemblyInfo.cs
+++ b/MyScreenSaver/Properties/AssemblyInfo.cs
@@ -32,5 +32,5 @@
// Tüm değerleri belirtebilir veya varsayılan Derleme ve Düzeltme Numaralarını kullanmak için
// aşağıda gösterildiği gibi '*' kullanabilirsiniz:
// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.3")]
-[assembly: AssemblyFileVersion("1.0.0.3")]
+[assembly: AssemblyVersion("1.0.0.5")]
+[assembly: AssemblyFileVersion("1.0.0.5")]
diff --git a/MyScreenSaver/Properties/Settings.Designer.cs b/MyScreenSaver/Properties/Settings.Designer.cs
index c3a0939..e4fd856 100644
--- a/MyScreenSaver/Properties/Settings.Designer.cs
+++ b/MyScreenSaver/Properties/Settings.Designer.cs
@@ -232,5 +232,18 @@ public bool MusicAppWMP {
this["MusicAppWMP"] = value;
}
}
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("\r\n")]
+ public global::System.Collections.Specialized.StringCollection VLC_URL {
+ get {
+ return ((global::System.Collections.Specialized.StringCollection)(this["VLC_URL"]));
+ }
+ set {
+ this["VLC_URL"] = value;
+ }
+ }
}
}
diff --git a/MyScreenSaver/Properties/Settings.settings b/MyScreenSaver/Properties/Settings.settings
index e1a1ad3..89b9cc7 100644
--- a/MyScreenSaver/Properties/Settings.settings
+++ b/MyScreenSaver/Properties/Settings.settings
@@ -59,5 +59,9 @@
False
+
+ <?xml version="1.0" encoding="utf-16"?>
+<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
+
\ No newline at end of file
diff --git a/MyScreenSaver/Units/VLC/README.md b/MyScreenSaver/Units/VLC/README.md
new file mode 100644
index 0000000..c4ac357
--- /dev/null
+++ b/MyScreenSaver/Units/VLC/README.md
@@ -0,0 +1 @@
+[https://github.com/videolan/vlc/blob/master/share/lua/playlist/youtube.lua](https://raw.githubusercontent.com/videolan/vlc/master/share/lua/playlist/youtube.lua)
\ No newline at end of file
diff --git a/MyScreenSaver/Units/VLC/youtube.lua b/MyScreenSaver/Units/VLC/youtube.lua
new file mode 100644
index 0000000..29bd00e
--- /dev/null
+++ b/MyScreenSaver/Units/VLC/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, '