pastebin

Paste Search Dynamic
Recent pastes
youtube.lua
  1. --[[
  2.  $Id$
  3.  
  4.  Copyright © 2007-2020 the VideoLAN team
  5.  
  6.  This program is free software; you can redistribute it and/or modify
  7.  it under the terms of the GNU General Public License as published by
  8.  the Free Software Foundation; either version 2 of the License, or
  9.  (at your option) any later version.
  10.  
  11.  This program is distributed in the hope that it will be useful,
  12.  but WITHOUT ANY WARRANTY; without even the implied warranty of
  13.  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14.  GNU General Public License for more details.
  15.  
  16.  You should have received a copy of the GNU General Public License
  17.  along with this program; if not, write to the Free Software
  18.  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
  19. --]]
  20.  
  21. -- Helper function to get a parameter's value in a URL
  22. function get_url_param( url, name )
  23.     local _, _, res = string.find( url, "[&?]"..name.."=([^&]*)" )
  24.     return res
  25. end
  26.  
  27. -- Helper function to copy a parameter when building a new URL
  28. function copy_url_param( url, name )
  29.     local value = get_url_param( url, name )
  30.     return ( value and "&"..name.."="..value or "" ) -- Ternary operator
  31. end
  32.  
  33. function get_arturl()
  34.     local iurl = get_url_param( vlc.path, "iurl" )
  35.     if iurl then
  36.         return iurl
  37.     end
  38.     local video_id = get_url_param( vlc.path, "v" )
  39.     if not video_id then
  40.         return nil
  41.     end
  42.     return vlc.access.."://img.youtube.com/vi/"..video_id.."/default.jpg"
  43. end
  44.  
  45. -- Pick the most suited format available
  46. function get_fmt( fmt_list )
  47.     local prefres = vlc.var.inherit(nil, "preferred-resolution")
  48.     if prefres < 0 then
  49.         return nil
  50.     end
  51.  
  52.     local fmt = nil
  53.     for itag,height in string.gmatch( fmt_list, "(%d+)/%d+x(%d+)[^,]*" ) do
  54.         -- Apparently formats are listed in quality
  55.         -- order, so we take the first one that works,
  56.         -- or fallback to the lowest quality
  57.         fmt = itag
  58.         if tonumber(height) <= prefres then
  59.             break
  60.         end
  61.     end
  62.     return fmt
  63. end
  64.  
  65. -- Buffering iterator to parse through the HTTP stream several times
  66. -- without making several HTTP requests
  67. function buf_iter( s )
  68.     s.i = s.i + 1
  69.     local line = s.lines[s.i]
  70.     if not line then
  71.         -- Put back together statements split across several lines,
  72.         -- otherwise we won't be able to parse them
  73.         repeat
  74.             local l = s.stream:readline()
  75.             if not l then break end
  76.             line = line and line..l or l -- Ternary operator
  77.         until string.match( line, "};$" )
  78.  
  79.         if line then
  80.             s.lines[s.i] = line
  81.         end
  82.     end
  83.     return line
  84. end
  85.  
  86. -- Helper to search and extract code from javascript stream
  87. function js_extract( js, pattern )
  88.     js.i = 0 -- Reset to beginning
  89.     for line in buf_iter, js do
  90.         local ex = string.match( line, pattern )
  91.         if ex then
  92.             return ex
  93.         end
  94.     end
  95.     vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
  96.     return nil
  97. end
  98.  
  99. -- Descramble the URL signature using the javascript code that does that
  100. -- in the web page
  101. function js_descramble( sig, js_url )
  102.     -- Fetch javascript code
  103.     local js = { stream = vlc.stream( js_url ), lines = {}, i = 0 }
  104.     if not js.stream then
  105.         vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
  106.         return sig
  107.     end
  108.  
  109.     -- Look for the descrambler function's name
  110.     -- if(k.s){var l=k.sp,m=pt(decodeURIComponent(k.s));f.set(l,encodeURIComponent(m))}
  111.     -- k.s (from stream map field "s") holds the input scrambled signature
  112.     -- k.sp (from stream map field "sp") holds a parameter name (normally
  113.     -- "signature" or "sig") to set with the output, descrambled signature
  114.     local descrambler = js_extract( js, "[=%(,&|](..)%(decodeURIComponent%(.%.s%)%)" )
  115.     if not descrambler then
  116.         vlc.msg.dbg( "Couldn't extract youtube video URL signature descrambling function name" )
  117.         return sig
  118.     end
  119.  
  120.     -- Fetch the code of the descrambler function
  121.     -- 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("")};
  122.     local rules = js_extract( js, "^"..descrambler.."=function%([^)]*%){(.-)};" )
  123.     if not rules then
  124.         vlc.msg.dbg( "Couldn't extract youtube video URL signature descrambling rules" )
  125.         return sig
  126.     end
  127.  
  128.     -- Get the name of the helper object providing transformation definitions
  129.     local helper = string.match( rules, ";(..)%...%(" )
  130.     if not helper then
  131.         vlc.msg.dbg( "Couldn't extract youtube video URL signature transformation helper name" )
  132.         vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
  133.         return sig
  134.     end
  135.  
  136.     -- Fetch the helper object code
  137.     -- 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)}};
  138.     local transformations = js_extract( js, "[ ,]"..helper.."={(.-)};" )
  139.     if not transformations then
  140.         vlc.msg.dbg( "Couldn't extract youtube video URL signature transformation code" )
  141.         return sig
  142.     end
  143.  
  144.     -- Parse the helper object to map available transformations
  145.     local trans = {}
  146.     for meth,code in string.gmatch( transformations, "(..):function%([^)]*%){([^}]*)}" ) do
  147.         -- a=a.reverse()
  148.         if string.match( code, "%.reverse%(" ) then
  149.           trans[meth] = "reverse"
  150.  
  151.         -- a.splice(0,b)
  152.         elseif string.match( code, "%.splice%(") then
  153.           trans[meth] = "slice"
  154.  
  155.         -- var c=a[0];a[0]=a[b%a.length];a[b]=c
  156.         elseif string.match( code, "var c=" ) then
  157.           trans[meth] = "swap"
  158.         else
  159.             vlc.msg.warn("Couldn't parse unknown youtube video URL signature transformation")
  160.         end
  161.     end
  162.  
  163.     -- Parse descrambling rules, map them to known transformations
  164.     -- and apply them on the signature
  165.     local missing = false
  166.     for meth,idx in string.gmatch( rules, "..%.(..)%([^,]+,(%d+)%)" ) do
  167.         idx = tonumber( idx )
  168.  
  169.         if trans[meth] == "reverse" then
  170.             sig = string.reverse( sig )
  171.  
  172.         elseif trans[meth] == "slice" then
  173.             sig = string.sub( sig, idx + 1 )
  174.  
  175.         elseif trans[meth] == "swap" then
  176.             if idx > 1 then
  177.                 sig = string.gsub( sig, "^(.)("..string.rep( ".", idx - 1 )..")(.)(.*)$", "%3%2%1%4" )
  178.             elseif idx == 1 then
  179.                 sig = string.gsub( sig, "^(.)(.)", "%2%1" )
  180.             end
  181.         else
  182.             vlc.msg.dbg("Couldn't apply unknown youtube video URL signature transformation")
  183.             missing = true
  184.         end
  185.     end
  186.     if missing then
  187.         vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
  188.     end
  189.     return sig
  190. end
  191.  
  192. -- Parse and assemble video stream URL
  193. function stream_url( params, js_url )
  194.     local url = string.match( params, "url=([^&]+)" )
  195.     if not url then
  196.         return nil
  197.     end
  198.     url = vlc.strings.decode_uri( url )
  199.  
  200.     -- Descramble any scrambled signature and append it to URL
  201.     local s = string.match( params, "s=([^&]+)" )
  202.     if s then
  203.         s = vlc.strings.decode_uri( s )
  204.         vlc.msg.dbg( "Found "..string.len( s ).."-character scrambled signature for youtube video URL, attempting to descramble... " )
  205.         if js_url then
  206.             s = js_descramble( s, js_url )
  207.         else
  208.             vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
  209.         end
  210.  
  211.         local sp = string.match( params, "sp=([^&]+)" )
  212.         if not sp then
  213.             vlc.msg.warn( "Couldn't extract signature parameters for youtube video URL, guessing" )
  214.             sp = "signature"
  215.         end
  216.         url = url.."&"..sp.."="..vlc.strings.encode_uri_component( s )
  217.     end
  218.  
  219.     return url
  220. end
  221.  
  222. -- Parse and pick our video stream URL (classic parameters)
  223. function pick_url( url_map, fmt, js_url )
  224.     for stream in string.gmatch( url_map, "[^,]+" ) do
  225.         local itag = string.match( stream, "itag=(%d+)" )
  226.         if not fmt or not itag or tonumber( itag ) == tonumber( fmt ) then
  227.             return stream_url( stream, js_url )
  228.         end
  229.     end
  230.     return nil
  231. end
  232.  
  233. -- Parse and pick our video stream URL (new-style parameters)
  234. function pick_stream( stream_map, js_url )
  235.     local pick = nil
  236.  
  237.     local fmt = tonumber( get_url_param( vlc.path, "fmt" ) )
  238.     if fmt then
  239.         -- Legacy match from URL parameter
  240.         for stream in string.gmatch( stream_map, '{(.-)}' ) do
  241.             local itag = tonumber( string.match( stream, '"itag":(%d+)' ) )
  242.             if fmt == itag then
  243.                 pick = stream
  244.                 break
  245.             end
  246.         end
  247.     else
  248.         -- Compare the different available formats listed with our
  249.         -- quality targets
  250.         local prefres = vlc.var.inherit( nil, "preferred-resolution" )
  251.         local bestres = nil
  252.  
  253.         for stream in string.gmatch( stream_map, '{(.-)}' ) do
  254.             local height = tonumber( string.match( stream, '"height":(%d+)' ) )
  255.  
  256.             -- Better than nothing
  257.             if not pick or ( height and ( not bestres
  258.                 -- Better quality within limits
  259.                 or ( ( prefres < 0 or height <= prefres ) and height > bestres )
  260.                 -- Lower quality more suited to limits
  261.                 or ( prefres > -1 and bestres > prefres and height < bestres )
  262.             ) ) then
  263.                 bestres = height
  264.                 pick = stream
  265.             end
  266.         end
  267.     end
  268.  
  269.     if not pick then
  270.         return nil
  271.     end
  272.  
  273.     -- Either the "url" or the "cipher" parameter is present,
  274.     -- depending on whether the URL signature is scrambled.
  275.     local cipher = string.match( pick, '"cipher":"(.-)"' )
  276.     if cipher then
  277.         -- Scrambled signature: some assembly required
  278.         local url = stream_url( cipher, js_url )
  279.         if url then
  280.             return url
  281.         end
  282.     end
  283.     -- Unscrambled signature, already included in ready-to-use URL
  284.     return string.match( pick, '"url":"(.-)"' )
  285. end
  286.  
  287. -- Probe function.
  288. function probe()
  289.     return ( ( vlc.access == "http" or vlc.access == "https" )
  290.              and (
  291.                string.match( vlc.path, "^www%.youtube%.com/" )
  292.             or string.match( vlc.path, "^gaming%.youtube%.com/" )
  293.              ) and (
  294.                string.match( vlc.path, "/watch%?" ) -- the html page
  295.             or string.match( vlc.path, "/live$" ) -- user live stream html page
  296.             or string.match( vlc.path, "/live%?" ) -- user live stream html page
  297.             or string.match( vlc.path, "/get_video_info%?" ) -- info API
  298.             or string.match( vlc.path, "/v/" ) -- video in swf player
  299.             or string.match( vlc.path, "/embed/" ) -- embedded player iframe
  300.              ) )
  301. end
  302.  
  303. -- Parse function.
  304. function parse()
  305.     if string.match( vlc.path, "^gaming%.youtube%.com/" ) then
  306.         url = string.gsub( vlc.path, "^gaming%.youtube%.com", "www.youtube.com" )
  307.         return { { path = vlc.access.."://"..url } }
  308.     end
  309.     if string.match( vlc.path, "/watch%?" )
  310.         or string.match( vlc.path, "/live$" )
  311.         or string.match( vlc.path, "/live%?" )
  312.     then -- This is the HTML page's URL
  313.         -- fmt is the format of the video
  314.         -- (cf. http://en.wikipedia.org/wiki/YouTube#Quality_and_formats)
  315.         fmt = get_url_param( vlc.path, "fmt" )
  316.         while true do
  317.             -- Try to find the video's title
  318.             line = vlc.readline()
  319.             if not line then break end
  320.             if string.match( line, "<meta property=\"og:title\"" ) then
  321.                 _,_,name = string.find( line, "content=\"(.-)\"" )
  322.                 name = vlc.strings.resolve_xml_special_chars( name )
  323.                 name = vlc.strings.resolve_xml_special_chars( name )
  324.             end
  325.  
  326.             if not description then
  327.                 description = string.match( line, "<p id=\"eow%-description\"[^>]*>(.-)</p>" )
  328.                 if description then
  329.                     description = vlc.strings.resolve_xml_special_chars( description )
  330.                 end
  331.             end
  332.  
  333.  
  334.             if string.match( line, "<meta property=\"og:image\"" ) then
  335.                 _,_,arturl = string.find( line, "content=\"(.-)\"" )
  336.                 arturl = vlc.strings.resolve_xml_special_chars( arturl )
  337.             end
  338.  
  339.             if not artist then
  340.                 artist = string.match(line, '\\"author\\":\\"(.-)\\"')
  341.             end
  342.  
  343.             -- JSON parameters, also formerly known as "swfConfig",
  344.             -- "SWF_ARGS", "swfArgs", "PLAYER_CONFIG", "playerConfig" ...
  345.             if string.match( line, "ytplayer%.config" ) then
  346.  
  347.                 local js_url = string.match( line, "\"js\": *\"(.-)\"" )
  348.                 if js_url then
  349.                     js_url = string.gsub( js_url, "\\/", "/" )
  350.                     -- Resolve URL
  351.                     if string.match( js_url, "^/[^/]" ) then
  352.                         local authority = string.match( vlc.path, "^([^/]*)/" )
  353.                         js_url = "//"..authority..js_url
  354.                     end
  355.                     js_url = string.gsub( js_url, "^//", vlc.access.."://" )
  356.                 end
  357.  
  358.                 -- Classic parameters
  359.                 if not fmt then
  360.                     fmt_list = string.match( line, "\"fmt_list\": *\"(.-)\"" )
  361.                     if fmt_list then
  362.                         fmt_list = string.gsub( fmt_list, "\\/", "/" )
  363.                         fmt = get_fmt( fmt_list )
  364.                     end
  365.                 end
  366.  
  367.                 url_map = string.match( line, "\"url_encoded_fmt_stream_map\": *\"(.-)\"" )
  368.                 if url_map then
  369.                     vlc.msg.dbg( "Found classic parameters for youtube video stream, parsing..." )
  370.                     -- FIXME: do this properly
  371.                     url_map = string.gsub( url_map, "\\u0026", "&" )
  372.                     path = pick_url( url_map, fmt, js_url )
  373.                 end
  374.  
  375.                 -- New-style parameters
  376.                 if not path then
  377.                     local stream_map = string.match( line, '\\"formats\\":%[(.-)%]' )
  378.                     if stream_map then
  379.                         vlc.msg.dbg( "Found new-style parameters for youtube video stream, parsing..." )
  380.                         stream_map = string.gsub( stream_map, '\\(["\\/])', '%1' )
  381.                         -- FIXME: do this properly
  382.                         stream_map = string.gsub( stream_map, "\\u0026", "&" )
  383.                         path = pick_stream( stream_map, js_url )
  384.                     end
  385.                 end
  386.  
  387.                 if not path then
  388.                     -- If this is a live stream, the URL map will be empty
  389.                     -- and we get the URL from this field instead
  390.                     local hlsvp = string.match( line, '\\"hlsManifestUrl\\": *\\"(.-)\\"' )
  391.                     if hlsvp then
  392.                         hlsvp = string.gsub( hlsvp, "\\/", "/" )
  393.                         path = hlsvp
  394.                     end
  395.                 end
  396.             end
  397.         end
  398.  
  399.         if not path then
  400.             local video_id = get_url_param( vlc.path, "v" )
  401.             if video_id then
  402.                 -- Passing no "el" parameter to /get_video_info seems to
  403.                 -- let it default to "embedded", and both known values
  404.                 -- of "embedded" and "detailpage" have historically been
  405.                 -- wrong and failed for various restricted videos.
  406.                 path = vlc.access.."://www.youtube.com/get_video_info?video_id="..video_id..copy_url_param( vlc.path, "fmt" )
  407.                 vlc.msg.warn( "Couldn't extract video URL, falling back to alternate youtube API" )
  408.             end
  409.         end
  410.  
  411.         if not path then
  412.             vlc.msg.err( "Couldn't extract youtube video URL, please check for updates to this script" )
  413.             return { }
  414.         end
  415.  
  416.         if not arturl then
  417.             arturl = get_arturl()
  418.         end
  419.  
  420.         return { { path = path; name = name; description = description; artist = artist; arturl = arturl } }
  421.  
  422.     elseif string.match( vlc.path, "/get_video_info%?" ) then -- video info API
  423.         local line = vlc.readline() -- data is on one line only
  424.  
  425.         -- Classic parameters
  426.         local fmt = get_url_param( vlc.path, "fmt" )
  427.         if not fmt then
  428.             local fmt_list = string.match( line, "&fmt_list=([^&]*)" )
  429.             if fmt_list then
  430.                 fmt_list = vlc.strings.decode_uri( fmt_list )
  431.                 fmt = get_fmt( fmt_list )
  432.             end
  433.         end
  434.  
  435.         local url_map = string.match( line, "&url_encoded_fmt_stream_map=([^&]*)" )
  436.         if url_map then
  437.             vlc.msg.dbg( "Found classic parameters for youtube video stream, parsing..." )
  438.             url_map = vlc.strings.decode_uri( url_map )
  439.             path = pick_url( url_map, fmt )
  440.         end
  441.  
  442.         -- New-style parameters
  443.         if not path then
  444.             local stream_map = string.match( line, '%%22formats%%22%%3A%%5B(.-)%%5D' )
  445.             if stream_map then
  446.                 vlc.msg.dbg( "Found new-style parameters for youtube video stream, parsing..." )
  447.                 stream_map = vlc.strings.decode_uri( stream_map )
  448.                 -- FIXME: do this properly
  449.                 stream_map = string.gsub( stream_map, "\\u0026", "&" )
  450.                 path = pick_stream( stream_map )
  451.             end
  452.         end
  453.  
  454.         if not path then
  455.             -- If this is a live stream, the URL map will be empty
  456.             -- and we get the URL from this field instead
  457.             local hlsvp = string.match( line, "%%22hlsManifestUrl%%22%%3A%%22(.-)%%22" )
  458.             if hlsvp then
  459.                 hlsvp = vlc.strings.decode_uri( hlsvp )
  460.                 path = hlsvp
  461.             end
  462.         end
  463.  
  464.         if not path then
  465.             vlc.msg.err( "Couldn't extract youtube video URL, please check for updates to this script" )
  466.             return { }
  467.         end
  468.  
  469.         local title = string.match( line, "%%22title%%22%%3A%%22(.-)%%22" )
  470.         if title then
  471.             title = string.gsub( title, "+", " " )
  472.             title = vlc.strings.decode_uri( title )
  473.         end
  474.         local artist = string.match( line, "%%22author%%22%%3A%%22(.-)%%22" )
  475.         if artist then
  476.             artist = string.gsub( artist, "+", " " )
  477.             artist = vlc.strings.decode_uri( artist )
  478.         end
  479.         local arturl = string.match( line, "%%22playerMicroformatRenderer%%22%%3A%%7B%%22thumbnail%%22%%3A%%7B%%22thumbnails%%22%%3A%%5B%%7B%%22url%%22%%3A%%22(.-)%%22" )
  480.         if arturl then
  481.             arturl = vlc.strings.decode_uri( arturl )
  482.         end
  483.  
  484.         return { { path = path, title = title, artist = artist, arturl = arturl } }
  485.  
  486.     else -- Other supported URL formats
  487.         local video_id = string.match( vlc.path, "/[^/]+/([^?]*)" )
  488.         if not video_id then
  489.             vlc.msg.err( "Couldn't extract youtube video URL" )
  490.             return { }
  491.         end
  492.         return { { path = vlc.access.."://www.youtube.com/watch?v="..video_id..copy_url_param( vlc.path, "fmt" ) } }
  493.     end
  494. end
Parsed in 0.052 seconds