Update NEWS for v0.9.20130619
[libquvi-scripts.git] / share / lua / website / bbc.lua
1
2 -- quvi
3 -- Copyright (C) 2011, 2012  quvi project
4 --
5 -- This file is part of quvi <http://quvi.sourceforge.net/>.
6 --
7 -- This library is free software; you can redistribute it and/or
8 -- modify it under the terms of the GNU Lesser General Public
9 -- License as published by the Free Software Foundation; either
10 -- version 2.1 of the License, or (at your option) any later version.
11 --
12 -- This library is distributed in the hope that it will be useful,
13 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
14 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 -- Lesser General Public License for more details.
16 --
17 -- You should have received a copy of the GNU Lesser General Public
18 -- License along with this library; if not, write to the Free Software
19 -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20 -- 02110-1301  USA
21
22
23 -- TODO:
24 -- - Add support for Radio programmes
25 -- - Add support for live streaming
26 -- - Better error messages for geolocation errors
27 -- - Offer the subtitles for download somehow
28
29 -- Obtained with grep -oP '(?<=service=")[^"]+(?=")' on config
30 local fmt_id_lookup = {
31   high     = 'iplayer_streaming_h264_flv_high',
32   standard = 'iplayer_streaming_h264_flv',
33   low      = 'iplayer_streaming_h264_flv_lo',
34   vlow     = 'iplayer_streaming_h264_flv_vlo'
35   -- iplayer_streaming_n95_3g
36   -- iplayer_streaming_n95_wifi
37 }
38
39 -- Identify the script.
40 function ident(self)
41     package.path = self.script_dir .. '/?.lua'
42     local C      = require 'quvi/const'
43     local r      = {}
44     r.domain     = "www.bbc.co.uk"
45     r.formats    = "default|best"
46     for k,_ in pairs(fmt_id_lookup) do
47         r.formats = r.formats .."|".. k
48     end
49     r.categories = C.proto_rtmp
50     local U      = require 'quvi/util'
51     r.handles    = U.handles(self.page_url, {r.domain}, {"/iplayer/"})
52     return r
53 end
54
55 -- Parse video URL.
56 function parse(self)
57
58     function needs_new_authString(params)
59         if not params['authString'] then
60             return false
61         end
62         local found = false
63         for _,kind in pairs{'limelight', 'akamai', 'level3', 'sis', 'iplayertok'} do
64             if kind == params['kind'] then
65                 found = true
66                 break
67             end
68         end
69         if not found then return false end
70         -- We don't need to check for the mode, we already know it's what we want
71         return true
72     end
73
74     function create_uri_for_limelight_level3_iplayertok(params)
75         params.uri = params.tcurl .. '/' .. params.playpath
76     end
77
78     function process_akamai(params)
79         params.playpath = params.identifier
80         params.application = params.application or 'ondemand'
81         params.application = params.application .. '?_fcs_vhost=' .. params.server .. '&undefined'
82         params.uri = 'rtmp://' .. params.server .. ':80/' .. params.application
83         if not params.authString:find("&aifp=") then
84             params.authString = params.authString .. '&aifp=v001'
85         end
86         if not params.authString:find("&slist=") then
87             params.identifier = params.identifier:gsub('^mp[34]:', '')
88             params.authString = params.authString .. '&slist=' .. params.identifier
89         end
90         params.playpath = params.playpath .. '?' .. params.authString
91         params.uri = params.uri .. '&' .. params.authString
92         params.application = params.application .. '&' .. params.authString
93         params.tcurl = 'rtmp://' .. params.server .. ':80/' .. params.application
94     end
95
96     function process_limelight_level3(params)
97         params.application = params.application .. '?' .. params.authString
98         params.tcurl = 'rtmp://' .. params.server .. ':1935/' .. params.application
99         params.playpath = params.identifier
100         create_uri_for_limelight_level3_iplayertok(params)
101     end
102
103     function process_iplayertok(params)
104         params.identifier = params.identifier .. '?' .. params.authString
105         params.playpath = params.identifier:gsub('^mp[34]:', '')
106         params.tcurl = 'rtmp://' .. params.server .. ':1935/' .. params.application
107         create_uri_for_limelight_level3_iplayertok(params)
108     end
109
110     self.host_id = 'bbc'
111
112     local _,_,s = self.page_url:find('episode/(.-)/')
113     local episode_id = s or error('no match: episode id')
114     self.id = episode_id
115
116     local playlist_uri =
117         'http://www.bbc.co.uk/iplayer/playlist/' .. episode_id
118     local playlist = quvi.fetch(playlist_uri, {fetch_type = 'playlist'})
119
120     local pl_item_p,_,s = playlist:find('<item kind="programme".-identifier="(.-)"')
121     if not s then
122         pl_item_p,_,s = playlist:find('<item kind="radioProgramme".-identifier="(.-)"')
123         -- TODO: Implement radio support
124         if s then
125             error('No support for radio yet')
126         end
127     end
128     local media_id = s or error('no match: media id')
129
130     local _,_,s = playlist:find('duration="(%d+)"', pl_item_p)
131     self.duration = tonumber(s) or 0
132
133     local _,_,s = playlist:find('<title>(.-)</title>')
134     self.title = s or error('no match: video title')
135
136     local _,_,s = playlist:find('<link rel="holding" href="(.-)"')
137     self.media_thumbnail_url = s or ""
138
139     -- stolen from http://lua-users.org/wiki/MathLibraryTutorial
140     math.randomseed(os.time()) math.random() math.random() math.random()
141     local config_uri =
142         'http://www.bbc.co.uk/mediaselector/4/mtis/stream/' ..
143         media_id .. "?cb=" .. math.random(10000)
144
145     local config = quvi.fetch(config_uri, {fetch_type = 'config'})
146
147     available_formats = {}
148     for fmt_id in config:gmatch("iplayer_streaming_[%w_]+") do
149         available_formats[fmt_id] = true
150     end
151     -- Create the list of acceptable formats, ordered by preference
152     local r = self.requested_format
153     local preferred = ((r == 'best') and {'high', 'standard', 'low', 'vlow'})
154                    or ((r == 'default') and {'standard', 'low', 'vlow', 'high'})
155                    or {r}
156
157     -- Pick the first acceptable format available
158     local format
159     for _, cur_format in ipairs(preferred) do
160         if available_formats[fmt_id_lookup[cur_format]] then
161             format = cur_format
162             break
163         end
164     end
165     if not format then error('format not available') end
166
167     -- Iterate over <media/>s
168     local media
169     for section in config:gmatch('<media .-</media>') do
170         if section:find('service="' .. fmt_id_lookup[format] .. '"') then
171             media = section
172             break
173         end
174     end
175     if not media then error("Couldn't parse the config") end
176
177     self.url = {}
178
179     -- Initialise with the default values from the media
180     local mparams = {}
181     for _,mparam in pairs{'kind', 'service'} do
182         _,_,mparams[mparam] = media:find(mparam .. '="(.-)"')
183         -- print ("MEDIA: mparams[" .. mparam .. "] = " .. mparams[mparam])
184     end
185
186     for connection in media:gmatch('<connection .-/>') do
187         local params, complete_uri = {}, ''
188
189         for _,param in pairs{'supplier', 'server', 'application', 'identifier', 'authString', 'kind'} do
190             _,_,params[param] = connection:find(param .. '="(.-)"')
191             -- print ("CONNECTION: params[" .. param .. "] = " .. (params[param] or "(null)"))
192         end
193
194         -- Get authstring from more specific mediaselector if
195         -- this mode is specified - fails sometimes otherwise
196         if needs_new_authString(params) then
197             local xml_url
198             xml_uri =
199                 'http://www.bbc.co.uk/mediaselector/4/mtis/stream/' ..
200                 media_id .. '/' .. mparams['service'] .. '/' .. params['kind'] ..
201                 "?cb=" .. math.random(10000)
202             xml = quvi.fetch(xml_uri, {fetch_type = 'config'})
203             local _,_,new_authString = xml:find('authString="(.-)"')
204             if new_authString then
205                 params['authString'] = new_authString:gsub('&amp;', '&')
206             end
207         else
208             -- Unescape the authString
209             if params['authString'] then
210                 params['authString'] = params['authString']:gsub('&amp;', '&')
211             end
212         end
213
214         -- in 'application', mp has a value containing one or more entries separated by strings.
215         -- We only keep the first entry.
216         if params.application then
217             params.application = params.application:gsub("&mp=([^,&]+),?.-&", "&mp=%1&")
218         end
219
220         if params.supplier == 'akamai' then
221             process_akamai(params)
222         end
223
224         if (params.supplier == 'limelight' or params.supplier == 'level3') then
225             process_limelight_level3(params)
226         end
227
228         params.uri = params.uri or error('Could not create RTMP URL')
229
230         complete_uri = params.uri
231             .. ' app=' .. params.application
232             .. ' playpath=' .. params.playpath
233             .. ' swfUrl=http://www.bbc.co.uk/emp/revisions/18269_21576_10player.swf?revision=18269_21576 swfVfy=1'
234             .. ' tcUrl=' .. params.tcurl
235             .. ' pageurl=' .. self.page_url
236
237         self.url[#(self.url) + 1] = complete_uri
238     end
239
240     return self
241 end