4 Copyright © 2007-2020 the VideoLAN team
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.
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.
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.
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
.."=([^&]*)" )
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
34 local iurl
= get_url_param( vlc
.path
, "iurl" )
38 local video_id
= get_url_param( vlc
.path
, "v" )
42 return vlc
.access
.."://img.youtube.com/vi/"..video_id
.."/default.jpg"
45 -- Pick the most suited format available
46 function get_fmt( fmt_list
)
47 local prefres
= vlc
.var
.inherit(nil, "preferred-resolution")
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
58 if tonumber(height
) <= prefres
then
65 -- Buffering iterator to parse through the HTTP stream several times
66 -- without making several HTTP requests
67 function buf_iter( s
)
69 local line
= s
.lines
[s
.i
]
71 -- Put back together statements split across several lines,
72 -- otherwise we won't be able to parse them
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
, "};$" )
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
)
95 vlc
.msg
.err( "Couldn't process youtube video URL, please check for updates to this script" )
99 -- Descramble the URL signature using the javascript code that does that
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" )
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" )
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%([^)]*%){(.-)};" )
124 vlc
.msg
.dbg( "Couldn't extract youtube video URL signature descrambling rules" )
128 -- Get the name of the helper object providing transformation definitions
129 local helper
= string.match( rules
, ";(..)%...%(" )
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" )
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" )
144 -- Parse the helper object to map available transformations
146 for meth
,code
in string.gmatch( transformations
, "(..):function%([^)]*%){([^}]*)}" ) do
148 if string.match( code
, "%.reverse%(" ) then
149 trans
[meth
] = "reverse"
152 elseif string.match( code
, "%.splice%(") then
153 trans
[meth
] = "slice"
155 -- var c=a[0];a[0]=a[b%a.length];a[b]=c
156 elseif string.match( code
, "var c=" ) then
159 vlc
.msg
.warn("Couldn't parse unknown youtube video URL signature transformation")
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
)
169 if trans
[meth
] == "reverse" then
170 sig
= string.reverse( sig
)
172 elseif trans
[meth
] == "slice" then
173 sig
= string.sub( sig
, idx
+ 1 )
175 elseif trans
[meth
] == "swap" then
177 sig
= string.gsub( sig
, "^(.)("..string.rep( ".", idx
- 1 )..")(.)(.*)$", "%3%2%1%4" )
179 sig
= string.gsub( sig
, "^(.)(.)", "%2%1" )
182 vlc
.msg
.dbg("Couldn't apply unknown youtube video URL signature transformation")
187 vlc
.msg
.err( "Couldn't process youtube video URL, please check for updates to this script" )
192 -- Parse and assemble video stream URL
193 function stream_url( params
, js_url
)
194 local url
= string.match( params
, "url=([^&]+)" )
198 url
= vlc
.strings
.decode_uri( url
)
200 -- Descramble any scrambled signature and append it to URL
201 local s
= string.match( params
, "s=([^&]+)" )
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... " )
206 s
= js_descramble( s
, js_url
)
208 vlc
.msg
.err( "Couldn't process youtube video URL, please check for updates to this script" )
211 local sp
= string.match( params
, "sp=([^&]+)" )
213 vlc
.msg
.warn( "Couldn't extract signature parameters for youtube video URL, guessing" )
216 url
= url
.."&"..sp
.."="..vlc
.strings
.encode_uri_component( s
)
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
)
233 -- Parse and pick our video stream URL (new-style parameters)
234 function pick_stream( stream_map
, js_url
)
237 local fmt
= tonumber( get_url_param( vlc
.path
, "fmt" ) )
239 -- Legacy match from URL parameter
240 for stream
in string.gmatch( stream_map
, '{(.-)}' ) do
241 local itag
= tonumber( string.match( stream
, '"itag":(%d+)' ) )
248 -- Compare the different available formats listed with our
250 local prefres
= vlc
.var
.inherit( nil, "preferred-resolution" )
253 for stream
in string.gmatch( stream_map
, '{(.-)}' ) do
254 local height
= tonumber( string.match( stream
, '"height":(%d+)' ) )
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
)
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":"(.-)"' )
277 -- Scrambled signature: some assembly required
278 local url
= stream_url( cipher
, js_url
)
283 -- Unscrambled signature, already included in ready-to-use URL
284 return string.match( pick
, '"url":"(.-)"' )
289 return ( ( vlc
.access
== "http" or vlc
.access
== "https" )
291 string.match( vlc
.path
, "^www%.youtube%.com/" )
292 or string.match( vlc
.path
, "^gaming%.youtube%.com/" )
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
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
} }
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" )
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
)
326 if not description
then
327 description
= string.match( line
, "<p id=\"eow%-description\"[^>]*>(.-)</p>" )
329 description
= vlc
.strings
.resolve_xml_special_chars( description
)
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
)
340 artist
= string.match(line
, '\\"author\\":\\"(.-)\\"')
343 -- JSON parameters, also formerly known as "swfConfig",
344 -- "SWF_ARGS", "swfArgs", "PLAYER_CONFIG", "playerConfig" ...
345 if string.match( line
, "ytplayer%.config" ) then
347 local js_url
= string.match( line
, "\"js\": *\"(.-)\"" )
349 js_url
= string.gsub( js_url
, "\\/", "/" )
351 if string.match( js_url
, "^/[^/]" ) then
352 local authority
= string.match( vlc
.path
, "^([^/]*)/" )
353 js_url
= "//"..authority
..js_url
355 js_url
= string.gsub( js_url
, "^//", vlc
.access
.."://" )
358 -- Classic parameters
360 fmt_list
= string.match( line
, "\"fmt_list\": *\"(.-)\"" )
362 fmt_list
= string.gsub( fmt_list
, "\\/", "/" )
363 fmt
= get_fmt( fmt_list
)
367 url_map
= string.match( line
, "\"url_encoded_fmt_stream_map\": *\"(.-)\"" )
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
)
375 -- New-style parameters
377 local stream_map
= string.match( line
, '\\"formats\\":%[(.-)%]' )
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
)
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\\": *\\"(.-)\\"' )
392 hlsvp
= string.gsub( hlsvp
, "\\/", "/" )
400 local video_id
= get_url_param( vlc
.path
, "v" )
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" )
412 vlc
.msg
.err( "Couldn't extract youtube video URL, please check for updates to this script" )
417 arturl
= get_arturl()
420 return { { path
= path
; name
= name
; description
= description
; artist
= artist
; arturl
= arturl
} }
422 elseif string.match( vlc
.path
, "/get_video_info%?" ) then -- video info API
423 local line
= vlc
.readline() -- data is on one line only
425 -- Classic parameters
426 local fmt
= get_url_param( vlc
.path
, "fmt" )
428 local fmt_list
= string.match( line
, "&fmt_list=([^&]*)" )
430 fmt_list
= vlc
.strings
.decode_uri( fmt_list
)
431 fmt
= get_fmt( fmt_list
)
435 local url_map
= string.match( line
, "&url_encoded_fmt_stream_map=([^&]*)" )
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
)
442 -- New-style parameters
444 local stream_map
= string.match( line
, '%%22formats%%22%%3A%%5B(.-)%%5D' )
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
)
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" )
459 hlsvp
= vlc
.strings
.decode_uri( hlsvp
)
465 vlc
.msg
.err( "Couldn't extract youtube video URL, please check for updates to this script" )
469 local title
= string.match( line
, "%%22title%%22%%3A%%22(.-)%%22" )
471 title
= string.gsub( title
, "+", " " )
472 title
= vlc
.strings
.decode_uri( title
)
474 local artist
= string.match( line
, "%%22author%%22%%3A%%22(.-)%%22" )
476 artist
= string.gsub( artist
, "+", " " )
477 artist
= vlc
.strings
.decode_uri( artist
)
479 local arturl
= string.match( line
, "%%22playerMicroformatRenderer%%22%%3A%%7B%%22thumbnail%%22%%3A%%7B%%22thumbnails%%22%%3A%%5B%%7B%%22url%%22%%3A%%22(.-)%%22" )
481 arturl
= vlc
.strings
.decode_uri( arturl
)
484 return { { path
= path
, title
= title
, artist
= artist
, arturl
= arturl
} }
486 else -- Other supported URL formats
487 local video_id
= string.match( vlc
.path
, "/[^/]+/([^?]*)" )
489 vlc
.msg
.err( "Couldn't extract youtube video URL" )
492 return { { path
= vlc
.access
.."://www.youtube.com/watch?v="..video_id
..copy_url_param( vlc
.path
, "fmt" ) } }