]> git.sesse.net Git - vlc/blob - share/lua/playlist/youtube.lua
youtube.lua: be more flexible with JSON spaces
[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         -- descrambler = string.match( line, "%.sig%|%|(.-)%(" )
94         descrambler = string.match( line, "%.sig||([a-zA-Z0-9]+)%(" )
95     end
96
97     -- Fetch the code of the descrambler function. The function is
98     -- conveniently preceded by the definition of a helper object
99     -- that it uses. Example:
100     -- 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)}};function Go(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("")};
101     local transformations = nil
102     local rules = nil
103     while not transformations and not rules do
104         local line
105         if #lines > 0 then
106             line = table.remove( lines )
107         else
108             line = js:readline()
109             if not line then
110                 vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
111                 return sig
112             end
113         end
114         transformations, rules = string.match( line, "var ..={(.-)};function "..descrambler.."%([^)]*%){(.-)}" )
115     end
116
117     -- Parse the helper object to map available transformations
118     local trans = {}
119     for meth,code in string.gmatch( transformations, "(..):function%([^)]*%){([^}]*)}" ) do
120         -- a=a.reverse()
121         if string.match( code, "%.reverse%(" ) then
122           trans[meth] = "reverse"
123
124         -- a.splice(0,b)
125         elseif string.match( code, "%.splice%(") then
126           trans[meth] = "slice"
127
128         -- var c=a[0];a[0]=a[b%a.length];a[b]=c
129         elseif string.match( code, "var c=" ) then
130           trans[meth] = "swap"
131         else
132             vlc.msg.warn("Couldn't parse unknown youtube video URL signature transformation")
133         end
134     end
135
136     -- Parse descrambling rules, map them to known transformations
137     -- and apply them on the signature
138     local missing = false
139     for meth,idx in string.gmatch( rules, "..%.(..)%([^,]+,(%d+)%)" ) do
140         idx = tonumber( idx )
141
142         if trans[meth] == "reverse" then
143             sig = string.reverse( sig )
144
145         elseif trans[meth] == "slice" then
146             sig = string.sub( sig, idx + 1 )
147
148         elseif trans[meth] == "swap" then
149             if idx > 1 then
150                 sig = string.gsub( sig, "^(.)("..string.rep( ".", idx - 1 )..")(.)(.*)$", "%3%2%1%4" )
151             elseif idx == 1 then
152                 sig = string.gsub( sig, "^(.)(.)", "%2%1" )
153             end
154         else
155             vlc.msg.dbg("Couldn't apply unknown youtube video URL signature transformation")
156             missing = true
157         end
158     end
159     if missing then
160         vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
161     end
162     return sig
163 end
164
165 -- Parse and pick our video URL
166 function pick_url( url_map, fmt, js_url )
167     local path = nil
168     for stream in string.gmatch( url_map, "[^,]+" ) do
169         -- Apparently formats are listed in quality order,
170         -- so we can afford to simply take the first one
171         local itag = string.match( stream, "itag=(%d+)" )
172         if not fmt or not itag or tonumber( itag ) == tonumber( fmt ) then
173             local url = string.match( stream, "url=([^&,]+)" )
174             if url then
175                 url = vlc.strings.decode_uri( url )
176
177                 local sig = string.match( stream, "sig=([^&,]+)" )
178                 if not sig then
179                     -- Scrambled signature
180                     sig = string.match( stream, "s=([^&,]+)" )
181                     if sig then
182                         vlc.msg.dbg( "Found "..string.len( sig ).."-character scrambled signature for youtube video URL, attempting to descramble... " )
183                         if js_url then
184                             sig = js_descramble( sig, js_url )
185                         else
186                             vlc.msg.err( "Couldn't process youtube video URL, please check for updates to this script" )
187                         end
188                     end
189                 end
190                 local signature = ""
191                 if sig then
192                     signature = "&signature="..sig
193                 end
194
195                 path = url..signature
196                 break
197             end
198         end
199     end
200     return path
201 end
202
203 -- Probe function.
204 function probe()
205     if vlc.access ~= "http" and vlc.access ~= "https" then
206         return false
207     end
208     youtube_site = string.match( string.sub( vlc.path, 1, 8 ), "youtube" )
209     if not youtube_site then
210         -- FIXME we should be using a builtin list of known youtube websites
211         -- like "fr.youtube.com", "uk.youtube.com" etc..
212         youtube_site = string.find( vlc.path, ".youtube.com" )
213         if youtube_site == nil then
214             return false
215         end
216     end
217     return (  string.match( vlc.path, "/watch%?" ) -- the html page
218             or string.match( vlc.path, "/get_video_info%?" ) -- info API
219             or string.match( vlc.path, "/v/" ) -- video in swf player
220             or string.match( vlc.path, "/embed/" ) -- embedded player iframe
221             or string.match( vlc.path, "/player2.swf" ) ) -- another player url
222 end
223
224 -- Parse function.
225 function parse()
226     if string.match( vlc.path, "/watch%?" )
227     then -- This is the HTML page's URL
228         -- fmt is the format of the video
229         -- (cf. http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs)
230         fmt = get_url_param( vlc.path, "fmt" )
231         while true do
232             -- Try to find the video's title
233             line = vlc.readline()
234             if not line then break end
235             if string.match( line, "<meta name=\"title\"" ) then
236                 _,_,name = string.find( line, "content=\"(.-)\"" )
237                 name = vlc.strings.resolve_xml_special_chars( name )
238                 name = vlc.strings.resolve_xml_special_chars( name )
239             end
240             if string.match( line, "<meta name=\"description\"" ) then
241                -- Don't ask me why they double encode ...
242                 _,_,description = string.find( line, "content=\"(.-)\"" )
243                 description = vlc.strings.resolve_xml_special_chars( description )
244                 description = vlc.strings.resolve_xml_special_chars( description )
245             end
246             if string.match( line, "<meta property=\"og:image\"" ) then
247                 _,_,arturl = string.find( line, "content=\"(.-)\"" )
248             end
249             -- This is not available in the video parameters (whereas it
250             -- is given by the get_video_info API as the "author" field)
251             if not artist then
252                 artist = string.match( line, "yt%-uix%-sessionlink yt%-user%-name[^>]*>([^<]*)</" )
253                 if artist then
254                     artist = vlc.strings.resolve_xml_special_chars( artist )
255                 end
256             end
257             -- JSON parameters, also formerly known as "swfConfig",
258             -- "SWF_ARGS", "swfArgs", "PLAYER_CONFIG", "playerConfig" ...
259             if string.match( line, "ytplayer%.config" ) then
260
261                 local js_url = string.match( line, "\"js\": *\"(.-)\"" )
262                 if js_url then
263                     js_url = string.gsub( js_url, "\\/", "/" )
264                     js_url = string.gsub( js_url, "^//", vlc.access.."://" )
265                 end
266
267                 if not fmt then
268                     fmt_list = string.match( line, "\"fmt_list\": *\"(.-)\"" )
269                     if fmt_list then
270                         fmt_list = string.gsub( fmt_list, "\\/", "/" )
271                         fmt = get_fmt( fmt_list )
272                     end
273                 end
274
275                 url_map = string.match( line, "\"url_encoded_fmt_stream_map\": *\"(.-)\"" )
276                 if url_map then
277                     -- FIXME: do this properly
278                     url_map = string.gsub( url_map, "\\u0026", "&" )
279                     path = pick_url( url_map, fmt, js_url )
280                 end
281
282                 if not path then
283                     -- If this is a live stream, the URL map will be empty
284                     -- and we get the URL from this field instead 
285                     local hlsvp = string.match( line, "\"hlsvp\": *\"(.-)\"" )
286                     if hlsvp then
287                         hlsvp = string.gsub( hlsvp, "\\/", "/" )
288                         path = hlsvp
289                     end
290                 end
291             -- There is also another version of the parameters, encoded
292             -- differently, as an HTML attribute of an <object> or <embed>
293             -- tag; but we don't need it now
294             end
295         end
296
297         if not path then
298             local video_id = get_url_param( vlc.path, "v" )
299             if video_id then
300                 if fmt then
301                     format = "&fmt=" .. fmt
302                 else
303                     format = ""
304                 end 
305                 -- Without "el=detailpage", /get_video_info fails for many
306                 -- music videos with errors about copyrighted content being
307                 -- "restricted from playback on certain sites"
308                 path = "http://www.youtube.com/get_video_info?video_id="..video_id..format.."&el=detailpage"
309                 vlc.msg.warn( "Couldn't extract video URL, falling back to alternate youtube API" )
310             end
311         end
312
313         if not path then
314             vlc.msg.err( "Couldn't extract youtube video URL, please check for updates to this script" )
315             return { }
316         end
317
318         if not arturl then
319             arturl = get_arturl()
320         end
321
322         return { { path = path; name = name; description = description; artist = artist; arturl = arturl } }
323
324     elseif string.match( vlc.path, "/get_video_info%?" ) then -- video info API
325         local line = vlc.readline() -- data is on one line only
326
327         local fmt = get_url_param( vlc.path, "fmt" )
328         if not fmt then
329             local fmt_list = string.match( line, "&fmt_list=([^&]*)" )
330             if fmt_list then
331                 fmt_list = vlc.strings.decode_uri( fmt_list )
332                 fmt = get_fmt( fmt_list )
333             end
334         end
335
336         local url_map = string.match( line, "&url_encoded_fmt_stream_map=([^&]*)" )
337         if url_map then
338             url_map = vlc.strings.decode_uri( url_map )
339             path = pick_url( url_map, fmt )
340         end
341
342         if not path then
343             -- If this is a live stream, the URL map will be empty
344             -- and we get the URL from this field instead 
345             local hlsvp = string.match( line, "&hlsvp=([^&]*)" )
346             if hlsvp then
347                 hlsvp = vlc.strings.decode_uri( hlsvp )
348                 path = hlsvp
349             end
350         end
351
352         if not path then
353             vlc.msg.err( "Couldn't extract youtube video URL, please check for updates to this script" )
354             return { }
355         end
356
357         local title = string.match( line, "&title=([^&]*)" )
358         if title then
359             title = string.gsub( title, "+", " " )
360             title = vlc.strings.decode_uri( title )
361         end
362         local artist = string.match( line, "&author=([^&]*)" )
363         if artist then
364             artist = string.gsub( artist, "+", " " )
365             artist = vlc.strings.decode_uri( artist )
366         end
367         local arturl = string.match( line, "&thumbnail_url=([^&]*)" )
368         if arturl then
369             arturl = vlc.strings.decode_uri( arturl )
370         end
371
372         return { { path = path, title = title, artist = artist, arturl = arturl } }
373
374     else -- This is the flash player's URL
375         video_id = get_url_param( vlc.path, "video_id" )
376         if not video_id then
377             _,_,video_id = string.find( vlc.path, "/v/([^?]*)" )
378         end
379         if not video_id then
380             video_id = string.match( vlc.path, "/embed/([^?]*)" )
381         end
382         if not video_id then
383             vlc.msg.err( "Couldn't extract youtube video URL" )
384             return { }
385         end
386         fmt = get_url_param( vlc.path, "fmt" )
387         if fmt then
388             format = "&fmt=" .. fmt
389         else
390             format = ""
391         end
392         return { { path = "http://www.youtube.com/watch?v="..video_id..format } }
393     end
394 end