Due to a change in the back-end CDN and streaming technology
[xbmc-dailyshow.git] / addon.py
blob518c180a473de029f052171b2b1d47d21b6d219e
1 import json
2 import os
3 from datetime import datetime
4 import re
5 import sys
6 import urllib
7 import urllib2
9 from BeautifulSoup import BeautifulSoup as BS3
10 from requests import get
11 from xbmc import translatePath as tp
12 from xbmc import log
13 import xbmc
14 import xbmcaddon
15 import xbmcgui
16 import xbmcplugin
18 addon = xbmcaddon.Addon('plugin.video.the.daily.show')
19 pluginhandle = int(sys.argv[1])
20 image_fanart = tp(os.path.join(addon.getAddonInfo('path'), 'fanart.jpg'))
21 image_fanart_search = tp(
22 os.path.join(addon.getAddonInfo('path'),
23 'fanart-search.jpg'))
24 xbmcplugin.setPluginFanart(pluginhandle, image_fanart, color2='0xFFFF3300')
25 TVShowTitle = 'The Daily Show'
27 if xbmcplugin.getSetting(pluginhandle, "sort") == '0':
28 SORTORDER = 'date'
29 elif xbmcplugin.getSetting(pluginhandle, "sort") == '1':
30 SORTORDER = 'views'
31 elif xbmcplugin.getSetting(pluginhandle, "sort") == '2':
32 SORTORDER = 'rating'
35 class Guest(object):
37 def __init__(self, data):
38 self.soup = data
40 def day(self):
41 raw_text = self.soup('a', {'class': 'full-episode-url'})[0].getText()
43 raw_text = raw_text.replace('Full Episode Available', '')
44 m = re.search(r'(.*) - .*', raw_text)
46 return m.group(1)
48 def name(self):
49 return self.soup('span', {'class': 'title'})[0].getText().replace('Exclusive - ', '')
51 def url(self):
52 return self.soup('a', {'class': 'imageHolder'})[0]['href']
55 class Episode:
57 def __init__(self, rtmp, bitrate, width, height):
58 self.rtmp = rtmp
59 self.bitrate = bitrate
60 self.width = width
61 self.height = height
63 @staticmethod
64 def fromUrlData(data):
65 # get attributes
66 m = re.search(
67 """width="([0-9]+).*height="([0-9]+).*bitrate="([0-9]+).*<src>(rtmpe[^<]+)</src>""",
68 data,
69 re.S)
70 if m:
71 (width, height, bitrate, rtmp) = m.groups()
72 return Episode(rtmp, int(bitrate), int(width), int(height))
74 # this should not happen
75 raise Exception
77 def __repr__(self):
78 return "rtmp: %s, bitrate: %i, width: %i, height: %i" % (self.rtmp,
79 self.bitrate, self.width, self.height)
82 def get_episodes(data):
83 # split urldata into renditions
84 renditions = re.findall("(<rendition.*?</rendition>)", data, re.S)
85 # return a list of episodes
86 return [Episode.fromUrlData(r) for r in renditions]
89 def get_settings_bitrate():
90 # map plugin settings to actual bitrates
91 setting2bitrate = {'1': 400, '2': 750, '3': 1200, '4': 1700, '5': 2200,
92 '6': 3500}
93 setting = xbmcplugin.getSetting(pluginhandle, "bitrate")
94 lbitrate = setting2bitrate.get(setting, 0)
95 return lbitrate
98 # Common
99 def get_url(url):
100 try:
101 log('The Daily Show --> get_url :: url = ' + url)
102 txdata = None
103 txheaders = {
104 'Referer': 'http://thedailyshow.cc.com/',
105 'X-Forwarded-For': '12.13.14.15',
106 'User-Agent':
107 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US;rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 ( .NET CLR 3.5.30729)',
109 req = urllib2.Request(url, txdata, txheaders)
110 response = urllib2.urlopen(req)
111 link = response.read()
112 response.close()
113 except urllib2.URLError as e:
114 log('Error code: ', e.code)
115 return False
116 else:
117 return link
120 def make_in_app_url(**kwargs):
121 data = json.dumps(kwargs)
122 quoted_data = urllib2.quote(data)
123 url = "{sysarg}?{data}".format(sysarg=sys.argv[0], data=quoted_data)
124 return url
127 def add_directory_entry(name, identifier):
128 """Adds a directory entry to the xbmc ListItem"""
129 url = make_in_app_url(mode=identifier)
130 liz = xbmcgui.ListItem(name, iconImage="DefaultFolder.png")
131 liz.setInfo(type="Video", infoLabels={"Title": name})
132 liz.setProperty('fanart_image', image_fanart)
133 xbmcplugin.addDirectoryItem(handle=pluginhandle, url=url,
134 listitem=liz, isFolder=True)
136 # Root listing
139 def root(**ignored):
140 msg = addon.getLocalizedString(30030)
141 add_directory_entry(msg, 'full')
142 #msg = addon.getLocalizedString(30032)
143 #add_directory_entry(msg, 'search')
144 #msg = addon.getLocalizedString(30033)
145 #add_directory_entry(msg, 'browse')
146 xbmcplugin.endOfDirectory(pluginhandle)
149 def show_message(title, message):
150 dialog = xbmcgui.Dialog()
151 dialog.ok(' %s ' % title, '%s ' % message)
153 def show_error(message):
154 show_message('Error', message)
156 def full_episodes(**ignored):
157 xbmcplugin.setContent(pluginhandle, 'episodes')
158 xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_NONE)
159 url = 'http://thedailyshow.cc.com/full-episodes/'
160 # Due to unstructured daily show site, there is no canonical JSON url
161 # so we find the full episode json url presented on the latest full episode
162 soup = BS3(get(url).text)
163 j = soup.head.script.text.strip().strip(';').split('=', 1)[1]
164 jj = json.loads(j)
165 jsonurl = None
166 for zone, attribs in jj.get('manifest').get('zones').items():
167 feed = attribs.get('feed')
168 urls = re.compile(r'http[^"]+/f1010/[^"]+').findall(feed)
169 if urls:
170 jsonurl = urls[0]
171 break
172 fallback_urls = re.compile(r'http[^"]+/f1013/[^"]+').findall(feed)
173 if fallback_urls:
174 jsonurl = fallback_urls[0]
175 break
176 if not jsonurl:
177 # give user feedback on problem here
178 errormsg = addon.getLocalizedString(30025)
179 show_error(errormsg)
180 return None
181 jsonresponse = json.loads(get(jsonurl).content)
182 episodes = jsonresponse.get('result').get('episodes') or []
183 if not episodes:
184 # give user feedback on problem here
185 errormsg = addon.getLocalizedString(30026)
186 show_error(errormsg)
187 return None
188 for episode in episodes:
189 thumbnail = None
190 if len(episode.get('images', ())) >= 1:
191 thumbnail = episode.get('images')[0].get('url')
192 airdate = episode.get('airDate', '0')
193 airdate = datetime.fromtimestamp(int(airdate)).strftime('%Y-%m-%d')
194 liz = xbmcgui.ListItem(
195 episode.get('title'),
196 iconImage="DefaultFolder.png",
197 thumbnailImage=thumbnail)
198 liz.setInfo(
199 type="Video", infoLabels={"Title": episode.get('title'),
200 "Plot":
201 episode.get('description'),
202 "Season": episode.get('season', {}).get('seasonNumber'),
203 "Episode": episode.get('season', {}).get('episodeNumber'),
204 "premiered": airdate,
205 "TVShowTitle": TVShowTitle})
206 liz.setProperty('IsPlayable', 'true')
207 liz.setProperty('fanart_image', image_fanart)
208 url = make_in_app_url(
209 mode="play_full_episode",
210 episode_id=episode.get('id'),
211 additional_data=episode,
213 log("make url: " + str(episode))
214 xbmcplugin.addDirectoryItem(handle=pluginhandle, url=url, listitem=liz)
216 xbmcplugin.endOfDirectory(pluginhandle)
221 def extract_search_results_from_response(response):
222 """Creates an xbmc DirectoryItem with ListItems of videos extracted from
223 the http results.
225 Returns False if there are no video urls found in the request.
226 Otherwise, returns True."""
228 soup = BS3(response.text)
229 results = soup.find('div', {'class': 'search-results'})
231 if results is None:
232 return False
234 for entry in results.findAll('div', {'class': 'entry'}):
235 video_page_url = entry.find('meta', dict(itemprop="url"))['content']
236 name = entry.find('meta', dict(itemprop="name"))['content']
237 description = entry.find(
238 'meta',
239 dict(itemprop="description"))['content']
240 thumbnail = entry.find(
241 'meta',
242 dict(itemprop="thumbnailUrl"))['content']
243 if thumbnail:
244 # strip unnecessary ?width= parameters that make the image too
245 # small
246 thumbnail = re.search(r"(.*)\?.*", thumbnail).groups()[0]
247 duration = None
248 duration_match = re.search(
249 'T(\d+)M',
250 entry.find('meta',
251 dict(itemprop="duration"))['content'])
252 if duration_match:
253 duration = duration_match.groups()[0]
254 upload_date = entry.find(
255 'meta',
256 dict(itemprop="uploadDate"))['content']
258 url = "{sysarg}?mode={mode}&name={name}&url={video_page_url}".format(
259 sysarg=sys.argv[0],
260 name=urllib.quote_plus(name),
261 video_page_url=urllib.quote_plus(video_page_url),
262 mode="play")
263 date_and_name = "%s - %s" % (upload_date.replace('-','/'), name)
264 liz = xbmcgui.ListItem(date_and_name, thumbnailImage=thumbnail)
265 liz.setInfo(type="Video", infoLabels={"Title": name,
266 "plot": description,
267 "premiered": upload_date,
268 "aired": upload_date,
269 "duration": duration,
270 "TVShowTitle": TVShowTitle
272 liz.setProperty('IsPlayable', 'true')
273 liz.setProperty('fanart_image', image_fanart_search)
274 xbmcplugin.addDirectoryItem(handle=pluginhandle, url=url, listitem=liz)
276 return True
279 def get_user_input(title, default="", hidden=False):
280 """Display a virtual keyboard to the user"""
282 keyboard = xbmc.Keyboard(default, title)
283 keyboard.setHiddenInput(hidden)
284 keyboard.doModal()
286 if keyboard.isConfirmed():
287 return keyboard.getText()
288 else:
289 return None
292 def search(**ignored):
293 msg = addon.getLocalizedString(30032)
294 query = get_user_input(msg)
295 if not query:
296 return
298 response = get(
299 "http://www.thedailyshow.com/videos",
300 params=dict(term=query))
302 if extract_search_results_from_response(response) is True:
303 xbmcplugin.endOfDirectory(pluginhandle)
304 else:
305 mydialogue = xbmcgui.Dialog()
306 msg = addon.getLocalizedString(30022)
307 mydialogue.ok(heading=TVShowTitle,
308 line1=msg)
311 def browse(**ignored):
312 """Browse videos by Date"""
313 mydialogue = xbmcgui.Dialog()
314 msg = addon.getLocalizedString(30020)
315 datestring = mydialogue.numeric(type=1, heading=msg)
317 if not datestring:
318 return
320 day, month, year = datestring.split("/")
322 urlstring = "http://www.thedailyshow.com/feeds/search" + \
323 "?startDate={year}-{month}-{day}&tags=&keywords=&sortOrder=desc&sortBy=date&page=1".format(
324 day=day,
325 month=month,
326 year=year)
328 response = get(urlstring)
330 if extract_search_results_from_response(response) is True:
331 xbmcplugin.endOfDirectory(pluginhandle)
332 else:
333 mydialogue = xbmcgui.Dialog()
334 msg = addon.getLocalizedString(30021)
335 mydialogue.ok(heading=TVShowTitle,
336 line1=msg)
339 def play_full_episode(episode_id, additional_data, **ignored):
340 content_id = 'mgid:arc:episode:thedailyshow.com:%s' % episode_id
341 url = 'http://thedailyshow.cc.com/feeds/mrss?uri=' + content_id
342 data = get_url(url)
343 uris = re.compile('<guid isPermaLink="false">(.+?)</guid>').findall(data)
344 stacked_url = 'stack://'
345 for uri in uris:
346 rtmp = grab_rtmp(uri)
347 stacked_url += rtmp.replace(',', ',,') + ' , '
348 stacked_url = stacked_url[:-3]
350 log('stacked_url --> %s' % stacked_url)
352 item = xbmcgui.ListItem("ignored", path=stacked_url)
353 xbmcplugin.setResolvedUrl(pluginhandle, True, item)
355 # Grab rtmp
358 def grab_rtmp(uri):
359 url = 'http://thedailyshow.cc.com/feeds/mediagen/?uri=' + uri
360 mp4_url = "http://mtvnmobile.vo.llnwd.net/kip0/_pxn=0+_pxK=18639+_pxE=/44620/mtvnorigin"
362 data = get_url(url)
363 episodes = get_episodes(data)
365 # sort episodes by bitrate ascending
366 episodes.sort(key=lambda x: x.bitrate)
368 # chose maximum bitrate by default
369 ep = episodes[-1]
371 # check user settings
372 lbitrate = get_settings_bitrate()
373 if lbitrate:
374 # use the largest bitrate smaller-or-equal to the user-chosen value
375 ep = filter(lambda x: x.bitrate <= lbitrate, episodes)[-1]
377 furl = mp4_url + ep.rtmp.split('viacomccstrm')[2]
378 log('furl --> %s' % furl)
379 return furl
382 mode_handlers = {
383 "browse": browse,
384 "full": full_episodes,
385 "play_full_episode": play_full_episode,
386 "search": search,
387 "root": root,
390 def main(data):
391 decoded = urllib2.unquote(data or "{}")
392 if len(decoded) >= 1 and decoded[0] == '?':
393 decoded = decoded[1:]
394 log('The Daily Show --> main :: decoded = ' + str(decoded))
395 parsed_data = json.loads(decoded)
396 mode = parsed_data.get('mode') or 'root'
397 mode_handlers[mode](**parsed_data)
399 main(sys.argv[2])