]> git.sesse.net Git - vlc/blob - share/lua/playlist/youtube.lua
9cadd1f1f8f29e283f88bb1d890ad2af880be7e0
[vlc] / share / lua / playlist / youtube.lua
1 --[[
2  $Id$
3
4  Copyright © 2007-2013 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 function get_arturl()
28     local iurl = get_url_param( vlc.path, "iurl" )
29     if iurl then
30         return iurl
31     end
32     local video_id = get_url_param( vlc.path, "v" )
33     if not video_id then
34         return nil
35     end
36     return "http://img.youtube.com/vi/"..video_id.."/default.jpg"
37 end
38
39 function get_prefres()
40     local prefres = -1
41     if vlc.var and vlc.var.inherit then
42         prefres = vlc.var.inherit(nil, "preferred-resolution")
43         if prefres == nil then
44             prefres = -1
45         end
46     end
47     return prefres
48 end
49
50 -- Pick the most suited format available
51 function get_fmt( fmt_list )
52     local prefres = get_prefres()
53     if prefres < 0 then
54         return nil
55     end
56
57     local fmt = nil
58     for itag,height in string.gmatch( fmt_list, "(%d+)/%d+x(%d+)/[^,]+" ) do
59         -- Apparently formats are listed in quality
60         -- order, so we take the first one that works,
61         -- or fallback to the lowest quality
62         fmt = itag
63         if tonumber(height) <= prefres then
64             break
65         end
66     end
67     return fmt
68 end
69
70 -- Descramble the URL signature using the javascript code that does that
71 -- in the web page
72 function js_descramble( sig, js_url )
73     -- Fetch javascript code
74     local js = vlc.stream( js_url )
75     if not js then
76         return sig
77     end
78     local lines = {}
79
80     -- Look for the descrambler function's name
81     local descrambler = nil
82     while not descrambler do
83         local line = js:readline()
84         if not line then
85             vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
86             return sig
87         end
88         -- Buffer lines for later, so we don't have to make a second
89         -- HTTP request later
90         table.insert( lines, line )
91         -- c&&(b.signature=ij(c));
92         descrambler = string.match( line, "%.signature=(.-)%(" )
93     end
94
95     -- Fetch the code of the descrambler function. Example:
96     -- function ij(a){a=a.split("");a=a.reverse();a=jj(a,12);a=jj(a,32);a=a.reverse();a=jj(a,34);a=a.slice(3);a=jj(a,35);a=jj(a,42);a=a.slice(2);return a.join("")}
97     local rules = nil
98     while not rules do
99         local line
100         if #lines > 0 then
101             line = table.remove( lines )
102         else
103             line = js:readline()
104             if not line then
105                 vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
106                 return sig
107             end
108         end
109         rules = string.match( line, "function "..descrambler.."%([^)]*%){(.-)}" )
110     end
111
112     -- Parse descrambling rules one by one and apply them on the
113     -- signature as we go
114     for rule in string.gmatch( rules, "[^;]+" ) do
115         -- a=a.reverse();
116         if string.match( rule, "%.reverse%(" ) then
117             sig = string.reverse( sig )
118         else
119
120         -- a=a.slice(3);
121         local len = string.match( rule, "%.slice%((%d+)%)" )
122         if len then
123             sig = string.sub( sig, len + 1 )
124         else
125
126         -- a=jj(a,32);
127         -- This is known to be a function swapping the first and nth
128         -- characters:
129         -- function jj(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c;return a}
130         local idx = string.match( rule, "=..%([^,]+,(%d+)%)" )
131         -- This swapping function may also appear inlined:
132         -- var b=a[0];a[0]=a[59%a.length];a[59]=b;
133         -- In that case we only catch one of the three rules.
134         if not idx then
135             idx = string.match( rule, ".%[(%d+)%]=." )
136         end
137         if idx then
138             idx = tonumber( idx )
139             if not idx then idx = 0 end
140             if idx > 1 then
141                 sig = string.gsub( sig, "^(.)("..string.rep( ".", idx - 1 )..")(.)(.*)$", "%3%2%1%4" )
142             elseif idx == 1 then
143                 sig = string.gsub( sig, "^(.)(.)", "%2%1" )
144             end
145         end end end
146
147         -- Simply ignore other statements, in particular initial split
148         -- and final join and return statements
149     end
150     return sig
151 end
152
153 function descramble81( sig )
154     sig = string.reverse( sig )
155     local s1,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13 =
156         string.match( sig, "(.)(.......................)(.)(..............)(.)(......)(.)(....)(.)(...................)(.)(........)(.)" )
157     return s3..s2..s5..s4..s1..s6..s13..s8..s7..s10..s9..s12..s11
158 end
159
160 local descramblers = {
161                        --[81] = descramble81
162                      }
163
164 function descramble( sig, js_url )
165     vlc.msg.dbg( "Found "..string.len( sig ).."-character scrambled signature for youtube video URL, attempting to descramble... " )
166     if js_url then
167         sig = js_descramble( sig, js_url )
168     else
169         local descrambler = descramblers[string.len( sig )]
170         if descrambler then
171             sig = descrambler( sig )
172         else
173             vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
174         end
175     end
176     return sig
177 end
178
179 -- Parse and pick our video URL
180 function pick_url( url_map, fmt, js_url )
181     local path = nil
182     for stream in string.gmatch( url_map, "[^,]+" ) do
183         -- Apparently formats are listed in quality order,
184         -- so we can afford to simply take the first one
185         local itag = string.match( stream, "itag=(%d+)" )
186         if not fmt or not itag or tonumber( itag ) == tonumber( fmt ) then
187             local url = string.match( stream, "url=([^&,]+)" )
188             if url then
189                 url = vlc.strings.decode_uri( url )
190
191                 local sig = string.match( stream, "sig=([^&,]+)" )
192                 if not sig then
193                     -- Scrambled signature
194                     sig = string.match( stream, "s=([^&,]+)" )
195                     if sig then
196                         sig = descramble( sig, js_url )
197                     end
198                 end
199                 local signature = ""
200                 if sig then
201                     signature = "&signature="..sig
202                 end
203
204                 path = url..signature
205                 break
206             end
207         end
208     end
209     return path
210 end
211
212 -- Probe function.
213 function probe()
214     if vlc.access ~= "http" and vlc.access ~= "https" then
215         return false
216     end
217     youtube_site = string.match( string.sub( vlc.path, 1, 8 ), "youtube" )
218     if not youtube_site then
219         -- FIXME we should be using a builtin list of known youtube websites
220         -- like "fr.youtube.com", "uk.youtube.com" etc..
221         youtube_site = string.find( vlc.path, ".youtube.com" )
222         if youtube_site == nil then
223             return false
224         end
225     end
226     return (  string.match( vlc.path, "/watch%?" ) -- the html page
227             or string.match( vlc.path, "/get_video_info%?" ) -- info API
228             or string.match( vlc.path, "/v/" ) -- video in swf player
229             or string.match( vlc.path, "/player2.swf" ) ) -- another player url
230 end
231
232 -- Parse function.
233 function parse()
234     if string.match( vlc.path, "/watch%?" )
235     then -- This is the HTML page's URL
236         -- fmt is the format of the video
237         -- (cf. http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs)
238         fmt = get_url_param( vlc.path, "fmt" )
239         while true do
240             -- Try to find the video's title
241             line = vlc.readline()
242             if not line then break end
243             if string.match( line, "<meta name=\"title\"" ) then
244                 _,_,name = string.find( line, "content=\"(.-)\"" )
245                 name = vlc.strings.resolve_xml_special_chars( name )
246                 name = vlc.strings.resolve_xml_special_chars( name )
247             end
248             if string.match( line, "<meta name=\"description\"" ) then
249                -- Don't ask me why they double encode ...
250                 _,_,description = string.find( line, "content=\"(.-)\"" )
251                 description = vlc.strings.resolve_xml_special_chars( description )
252                 description = vlc.strings.resolve_xml_special_chars( description )
253             end
254             if string.match( line, "<meta property=\"og:image\"" ) then
255                 _,_,arturl = string.find( line, "content=\"(.-)\"" )
256             end
257             -- This is not available in the video parameters (whereas it
258             -- is given by the get_video_info API as the "author" field)
259             if not artist then
260                 artist = string.match( line, "yt%-uix%-sessionlink yt%-user%-name[^>]*>([^<]*)</" )
261                 if artist then
262                     artist = vlc.strings.resolve_xml_special_chars( artist )
263                 end
264             end
265             -- JSON parameters, also formerly known as "swfConfig",
266             -- "SWF_ARGS", "swfArgs", "PLAYER_CONFIG", "playerConfig" ...
267             if string.match( line, "ytplayer%.config" ) then
268
269                 local js_url = string.match( line, "\"js\": \"(.-)\"" )
270                 if js_url then
271                     js_url = string.gsub( js_url, "\\/", "/" )
272                     js_url = string.gsub( js_url, "^//", vlc.access.."://" )
273                 end
274
275                 if not fmt then
276                     fmt_list = string.match( line, "\"fmt_list\": \"(.-)\"" )
277                     if fmt_list then
278                         fmt_list = string.gsub( fmt_list, "\\/", "/" )
279                         fmt = get_fmt( fmt_list )
280                     end
281                 end
282
283                 url_map = string.match( line, "\"url_encoded_fmt_stream_map\": \"(.-)\"" )
284                 if url_map then
285                     -- FIXME: do this properly
286                     url_map = string.gsub( url_map, "\\u0026", "&" )
287                     path = pick_url( url_map, fmt, js_url )
288                 end
289
290                 if not path then
291                     -- If this is a live stream, the URL map will be empty
292                     -- and we get the URL from this field instead 
293                     local hlsvp = string.match( line, "\"hlsvp\": \"(.-)\"" )
294                     if hlsvp then
295                         hlsvp = string.gsub( hlsvp, "\\/", "/" )
296                         path = hlsvp
297                     end
298                 end
299             -- There is also another version of the parameters, encoded
300             -- differently, as an HTML attribute of an <object> or <embed>
301             -- tag; but we don't need it now
302             end
303         end
304
305         if not path then
306             local video_id = get_url_param( vlc.path, "v" )
307             if video_id then
308                 if fmt then
309                     format = "&fmt=" .. fmt
310                 else
311                     format = ""
312                 end 
313                 -- Without "el=detailpage", /get_video_info fails for many
314                 -- music videos with errors about copyrighted content being
315                 -- "restricted from playback on certain sites"
316                 path = "http://www.youtube.com/get_video_info?video_id="..video_id..format.."&el=detailpage"
317                 vlc.msg.warn( "Couldn't extract video URL, falling back to alternate youtube API" )
318             end
319         end
320
321         if not path then
322             vlc.msg.err( "Couldn't extract youtube video URL, please check for updates to this script" )
323             return { }
324         end
325
326         if not arturl then
327             arturl = get_arturl()
328         end
329
330         return { { path = path; name = name; description = description; artist = artist; arturl = arturl } }
331
332     elseif string.match( vlc.path, "/get_video_info%?" ) then -- video info API
333         local line = vlc.readline() -- data is on one line only
334
335         local fmt = get_url_param( vlc.path, "fmt" )
336         if not fmt then
337             local fmt_list = string.match( line, "&fmt_list=([^&]*)" )
338             if fmt_list then
339                 fmt_list = vlc.strings.decode_uri( fmt_list )
340                 fmt = get_fmt( fmt_list )
341             end
342         end
343
344         local url_map = string.match( line, "&url_encoded_fmt_stream_map=([^&]*)" )
345         if url_map then
346             url_map = vlc.strings.decode_uri( url_map )
347             path = pick_url( url_map, fmt )
348         end
349
350         if not path then
351             -- If this is a live stream, the URL map will be empty
352             -- and we get the URL from this field instead 
353             local hlsvp = string.match( line, "&hlsvp=([^&]*)" )
354             if hlsvp then
355                 hlsvp = vlc.strings.decode_uri( hlsvp )
356                 path = hlsvp
357             end
358         end
359
360         if not path then
361             vlc.msg.err( "Couldn't extract youtube video URL, please check for updates to this script" )
362             return { }
363         end
364
365         local title = string.match( line, "&title=([^&]*)" )
366         if title then
367             title = string.gsub( title, "+", " " )
368             title = vlc.strings.decode_uri( title )
369         end
370         local artist = string.match( line, "&author=([^&]*)" )
371         if artist then
372             artist = string.gsub( artist, "+", " " )
373             artist = vlc.strings.decode_uri( artist )
374         end
375         local arturl = string.match( line, "&thumbnail_url=([^&]*)" )
376         if arturl then
377             arturl = vlc.strings.decode_uri( arturl )
378         end
379
380         return { { path = path, title = title, artist = artist, arturl = arturl } }
381
382     else -- This is the flash player's URL
383         video_id = get_url_param( vlc.path, "video_id" )
384         if not video_id then
385             _,_,video_id = string.find( vlc.path, "/v/([^?]*)" )
386         end
387         if not video_id then
388             vlc.msg.err( "Couldn't extract youtube video URL" )
389             return { }
390         end
391         fmt = get_url_param( vlc.path, "fmt" )
392         if fmt then
393             format = "&fmt=" .. fmt
394         else
395             format = ""
396         end
397         return { { path = "http://www.youtube.com/watch?v="..video_id..format } }
398     end
399 end