Add support for BBC iPlayer (#28)
[libquvi-scripts.git] / share / lua / website / bbc.lua
blobea6fcc192ef9d29e8ee8b3023040a59489cfb594
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.
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.
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
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
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
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
55 -- Parse video URL.
56 function parse(self)
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
74 function create_uri_for_limelight_level3_iplayertok(params)
75 params.uri = params.tcurl .. '/' .. params.playpath
76 end
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
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)
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)
110 self.host_id = 'bbc'
112 local _,_,s = self.page_url:find('episode/(.-)/')
113 local episode_id = s or error('no match: episode id')
114 self.id = episode_id
116 local playlist_uri =
117 'http://www.bbc.co.uk/iplayer/playlist/' .. episode_id
118 local playlist = quvi.fetch(playlist_uri, {fetch_type = 'playlist'})
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')
128 local media_id = s or error('no match: media id')
130 local _,_,s = playlist:find('duration="(%d+)"', pl_item_p)
131 self.duration = tonumber(s) or 0
133 local _,_,s = playlist:find('<title>(.-)</title>')
134 self.title = s or error('no match: video title')
136 local _,_,s = playlist:find('<link rel="holding" href="(.-)"')
137 self.media_thumbnail_url = s or ""
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)
145 local config = quvi.fetch(config_uri, {fetch_type = 'config'})
147 available_formats = {}
148 for fmt_id in config:gmatch("iplayer_streaming_[%w_]+") do
149 available_formats[fmt_id] = true
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}
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
165 if not format then error('format not available') end
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
175 if not media then error("Couldn't parse the config") end
177 self.url = {}
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])
186 for connection in media:gmatch('<connection .-/>') do
187 local params, complete_uri = {}, ''
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)"))
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;', '&')
207 else
208 -- Unescape the authString
209 if params['authString'] then
210 params['authString'] = params['authString']:gsub('&amp;', '&')
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&")
220 if params.supplier == 'akamai' then
221 process_akamai(params)
224 if (params.supplier == 'limelight' or params.supplier == 'level3') then
225 process_limelight_level3(params)
228 params.uri = params.uri or error('Could not create RTMP URL')
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
237 self.url[#(self.url) + 1] = complete_uri
240 return self