Enigma2 plugin to to play various online streams (mostly Latvian).

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. # -*- coding: utf-8 -*-
  2. """
  3. A Kodi plugin for Viaplay
  4. """
  5. import sys
  6. import os
  7. import urllib
  8. import urlparse
  9. from resources.lib.vialib import vialib
  10. import xbmc
  11. import xbmcaddon
  12. import xbmcvfs
  13. import xbmcgui
  14. import xbmcplugin
  15. addon = xbmcaddon.Addon()
  16. addon_path = xbmc.translatePath(addon.getAddonInfo('path'))
  17. addon_profile = xbmc.translatePath(addon.getAddonInfo('profile'))
  18. tempdir = os.path.join(addon_profile, 'tmp')
  19. language = addon.getLocalizedString
  20. logging_prefix = '[%s-%s]' % (addon.getAddonInfo('id'), addon.getAddonInfo('version'))
  21. if not xbmcvfs.exists(addon_profile):
  22. xbmcvfs.mkdir(addon_profile)
  23. if not xbmcvfs.exists(tempdir):
  24. xbmcvfs.mkdir(tempdir)
  25. _url = sys.argv[0] # get the plugin url in plugin:// notation
  26. _handle = int(sys.argv[1]) # get the plugin handle as an integer number
  27. username = addon.getSetting('email')
  28. password = addon.getSetting('password')
  29. cookie_file = os.path.join(addon_profile, 'cookie_file')
  30. deviceid_file = os.path.join(addon_profile, 'deviceId')
  31. if addon.getSetting('disable_ssl') == 'true':
  32. ssl = False
  33. else:
  34. ssl = True
  35. if addon.getSetting('debug') == 'false':
  36. debug = False
  37. else:
  38. debug = True
  39. if addon.getSetting('country') == '0':
  40. country = 'se'
  41. elif addon.getSetting('country') == '1':
  42. country = 'dk'
  43. elif addon.getSetting('country') == '2':
  44. country = 'no'
  45. else:
  46. country = 'fi'
  47. vp = vialib(username, password, cookie_file, deviceid_file, tempdir, country, ssl, debug)
  48. def addon_log(string):
  49. if debug:
  50. xbmc.log('%s: %s' % (logging_prefix, string))
  51. def show_auth_error(error):
  52. if error == 'UserNotAuthorizedForContentError':
  53. message = language(30020)
  54. elif error == 'PurchaseConfirmationRequiredError':
  55. message = language(30021)
  56. elif error == 'UserNotAuthorizedRegionBlockedError':
  57. message = language(30022)
  58. else:
  59. message = error
  60. show_dialog(dialog_type='ok', heading=language(30017), message=message)
  61. def root_menu():
  62. items = []
  63. data = vp.make_request(url=vp.base_url, method='get')
  64. categories = vp.get_categories(input=data, method='data')
  65. for category in categories:
  66. categorytype = category['type']
  67. videotype = category['name']
  68. title = category['title']
  69. if categorytype != 'editorial':
  70. if videotype == 'series':
  71. parameters = {'action': 'series_menu', 'url': category['href']}
  72. elif videotype == 'movie' or videotype == 'rental':
  73. parameters = {'action': 'movies_menu', 'url': category['href']}
  74. elif videotype == 'sport':
  75. parameters = {'action': 'sports_menu', 'url': category['href']}
  76. elif videotype == 'kids':
  77. parameters = {'action': 'kids_menu', 'url': category['href']}
  78. else:
  79. addon_log('Unsupported videotype found: %s' % videotype)
  80. parameters = {'action': 'show_dialog', 'dialog_type': 'ok', 'heading': language(30017),
  81. 'message': 'This type (%s) is not yet supported.' % videotype}
  82. items = add_item(title, parameters, items=items)
  83. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  84. list_search(data)
  85. xbmcplugin.endOfDirectory(_handle)
  86. def movies_menu(url):
  87. items = []
  88. categories = vp.get_categories(url)
  89. for category in categories:
  90. title = category['title']
  91. parameters = {'action': 'list_sortings', 'url': category['href']}
  92. items = add_item(title, parameters, items=items)
  93. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  94. xbmcplugin.endOfDirectory(_handle)
  95. def series_menu(url):
  96. items = []
  97. categories = vp.get_categories(url)
  98. for category in categories:
  99. title = category['title']
  100. parameters = {'action': 'list_sortings', 'url': category['href']}
  101. items = add_item(title, parameters, items=items)
  102. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  103. xbmcplugin.endOfDirectory(_handle)
  104. def kids_menu(url):
  105. items = []
  106. categories = vp.get_categories(url)
  107. for category in categories:
  108. title = '%s: %s' % (category['group']['title'].title(), category['title'])
  109. category_url = category['href']
  110. parameters = {'action': 'list_products', 'url': category_url}
  111. items = add_item(title, parameters, items=items)
  112. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  113. xbmcplugin.endOfDirectory(_handle)
  114. def list_sortings(url):
  115. items = []
  116. sortings = vp.get_sortings(url)
  117. if sortings:
  118. for sorting in sortings:
  119. title = sorting['title']
  120. sorting_url = sorting['href']
  121. try:
  122. if sorting['id'] == 'alphabetical':
  123. parameters = {'action': 'list_alphabetical_letters', 'url': sorting_url}
  124. else:
  125. parameters = {'action': 'list_products', 'url': sorting_url}
  126. except TypeError:
  127. parameters = {'action': 'list_products', 'url': sorting_url}
  128. items = add_item(title, parameters, items=items)
  129. list_products_alphabetical(url)
  130. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  131. xbmcplugin.endOfDirectory(_handle)
  132. def list_products_alphabetical(url):
  133. """List all products in alphabetical order."""
  134. title = language(30013)
  135. parameters = {'action': 'list_products', 'url': url + '?sort=alphabetical'}
  136. add_item(title, parameters)
  137. def list_alphabetical_letters(url):
  138. items = []
  139. letters = vp.get_letters(url)
  140. for letter in letters:
  141. if letter == '0-9':
  142. query = '#' # 0-9 needs to be sent as a number sign
  143. else:
  144. query = letter.lower()
  145. parameters = {'action': 'list_products', 'url': url + '&letter=' + urllib.quote(query)}
  146. items = add_item(letter, parameters, items=items)
  147. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  148. xbmcplugin.endOfDirectory(_handle)
  149. def list_next_page(data):
  150. if vp.get_next_page(data):
  151. title = language(30018)
  152. parameters = {'action': 'list_products', 'url': vp.get_next_page(data)}
  153. add_item(title, parameters)
  154. def list_products(url, filter_event=False):
  155. items = []
  156. data = vp.make_request(url=url, method='get')
  157. products = vp.get_products(input=data, method='data', filter_event=filter_event)
  158. for product in products:
  159. content = product['type']
  160. try:
  161. playid = product['system']['guid']
  162. streamtype = 'guid'
  163. except KeyError:
  164. """The guid is not always available from the category listing.
  165. Send the self URL and let play_video grab the guid from there instead
  166. as it always provides more detailed data about each product."""
  167. playid = product['_links']['self']['href']
  168. streamtype = 'url'
  169. parameters = {'action': 'play_video', 'playid': playid.encode('utf-8'), 'streamtype': streamtype,
  170. 'content': content}
  171. if content == 'episode':
  172. title = product['content']['series']['episodeTitle']
  173. playable = True
  174. watched = True
  175. set_content = 'episodes'
  176. elif content == 'sport':
  177. product_name = product['content']['title'].encode('utf-8')
  178. if product['event_status'] == 'archive':
  179. title = 'Archive: %s' % product_name
  180. else:
  181. title = '%s (%s)' % (product_name, product['event_date'].strftime('%H:%M'))
  182. if product['event_status'] == 'upcoming':
  183. parameters = {'action': 'show_dialog', 'dialog_type': 'ok', 'heading': language(30017),
  184. 'message': '%s %s.' % (language(30016), product['event_date'].strftime('%Y-%m-%d %H:%M'))}
  185. playable = False
  186. else:
  187. playable = True
  188. watched = False
  189. set_content = 'movies'
  190. elif content == 'movie':
  191. movie_name = product['content']['title'].encode('utf-8')
  192. movie_year = str(product['content']['production']['year'])
  193. title = '%s (%s)' % (movie_name, movie_year)
  194. if product['system']['availability']['planInfo']['isRental'] is True:
  195. title = title + ' *' # mark rental products with an asterisk
  196. playable = True
  197. watched = True
  198. set_content = 'movies'
  199. elif content == 'series':
  200. title = product['content']['series']['title'].encode('utf-8')
  201. season_url = product['_links']['viaplay:page']['href']
  202. parameters = {'action': 'list_seasons', 'url': season_url}
  203. playable = False
  204. watched = True
  205. set_content = 'tvshows'
  206. items = add_item(title, parameters, items=items, playable=playable, watched=watched, set_content=set_content,
  207. set_info=return_info(product, content), set_art=return_art(product, content))
  208. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  209. list_next_page(data)
  210. xbmcplugin.endOfDirectory(_handle)
  211. def list_seasons(url):
  212. """List all series seasons."""
  213. seasons = vp.get_seasons(url)
  214. if len(seasons) == 1:
  215. # list products if there's only one season
  216. season_url = seasons[0]['_links']['self']['href']
  217. list_products(season_url)
  218. else:
  219. items = []
  220. for season in seasons:
  221. season_url = season['_links']['self']['href']
  222. title = '%s %s' % (language(30014), season['title'])
  223. parameters = {'action': 'list_products', 'url': season_url}
  224. items = add_item(title, parameters, items=items)
  225. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  226. xbmcplugin.endOfDirectory(_handle)
  227. def return_info(product, content):
  228. """Return the product information in a xbmcgui.setInfo friendly dict.
  229. Supported content types: episode, series, movie, sport"""
  230. cast = []
  231. mediatype = None
  232. title = None
  233. tvshowtitle = None
  234. season = None
  235. episode = None
  236. plot = None
  237. director = None
  238. try:
  239. duration = int(product['content']['duration']['milliseconds']) / 1000
  240. except KeyError:
  241. duration = None
  242. try:
  243. imdb_code = product['content']['imdb']['id']
  244. except KeyError:
  245. imdb_code = None
  246. try:
  247. rating = float(product['content']['imdb']['rating'])
  248. except KeyError:
  249. rating = None
  250. try:
  251. votes = str(product['content']['imdb']['votes'])
  252. except KeyError:
  253. votes = None
  254. try:
  255. year = int(product['content']['production']['year'])
  256. except KeyError:
  257. year = None
  258. try:
  259. genres = []
  260. for genre in product['_links']['viaplay:genres']:
  261. genres.append(genre['title'])
  262. genre = ', '.join(genres)
  263. except KeyError:
  264. genre = None
  265. try:
  266. mpaa = product['content']['parentalRating']
  267. except KeyError:
  268. mpaa = None
  269. if content == 'episode':
  270. mediatype = 'episode'
  271. title = product['content']['series']['episodeTitle'].encode('utf-8')
  272. tvshowtitle = product['content']['series']['title'].encode('utf-8')
  273. season = int(product['content']['series']['season']['seasonNumber'])
  274. episode = int(product['content']['series']['episodeNumber'])
  275. plot = product['content']['synopsis'].encode('utf-8')
  276. elif content == 'series':
  277. mediatype = 'tvshow'
  278. title = product['content']['series']['title'].encode('utf-8')
  279. tvshowtitle = product['content']['series']['title'].encode('utf-8')
  280. try:
  281. plot = product['content']['series']['synopsis'].encode('utf-8')
  282. except KeyError:
  283. plot = product['content']['synopsis'].encode('utf-8') # needed for alphabetical listing
  284. elif content == 'movie':
  285. mediatype = 'movie'
  286. title = product['content']['title'].encode('utf-8')
  287. plot = product['content']['synopsis'].encode('utf-8')
  288. try:
  289. for actor in product['content']['people']['actors']:
  290. cast.append(actor)
  291. except KeyError:
  292. pass
  293. try:
  294. directors = []
  295. for director in product['content']['people']['directors']:
  296. directors.append(director)
  297. director = ', '.join(directors)
  298. except KeyError:
  299. pass
  300. elif content == 'sport':
  301. mediatype = 'video'
  302. title = product['content']['title'].encode('utf-8')
  303. plot = product['content']['synopsis'].encode('utf-8')
  304. info = {
  305. 'mediatype': mediatype,
  306. 'title': title,
  307. 'tvshowtitle': tvshowtitle,
  308. 'season': season,
  309. 'episode': episode,
  310. 'year': year,
  311. 'plot': plot,
  312. 'duration': duration,
  313. 'code': imdb_code,
  314. 'rating': rating,
  315. 'votes': votes,
  316. 'genre': genre,
  317. 'director': director,
  318. 'mpaa': mpaa,
  319. 'cast': cast
  320. }
  321. return info
  322. def return_art(product, content):
  323. """Return the available art in a xbmcgui.setArt friendly dict."""
  324. try:
  325. boxart = product['content']['images']['boxart']['url'].split('.jpg')[0] + '.jpg'
  326. except KeyError:
  327. boxart = None
  328. try:
  329. hero169 = product['content']['images']['hero169']['template'].split('.jpg')[0] + '.jpg'
  330. except KeyError:
  331. hero169 = None
  332. try:
  333. coverart23 = product['content']['images']['coverart23']['template'].split('.jpg')[0] + '.jpg'
  334. except KeyError:
  335. coverart23 = None
  336. try:
  337. coverart169 = product['content']['images']['coverart23']['template'].split('.jpg')[0] + '.jpg'
  338. except KeyError:
  339. coverart169 = None
  340. try:
  341. landscape = product['content']['images']['landscape']['url'].split('.jpg')[0] + '.jpg'
  342. except KeyError:
  343. landscape = None
  344. if content == 'episode' or content == 'sport':
  345. thumbnail = landscape
  346. else:
  347. thumbnail = boxart
  348. fanart = hero169
  349. banner = landscape
  350. cover = coverart23
  351. poster = boxart
  352. art = {
  353. 'thumb': thumbnail,
  354. 'fanart': fanart,
  355. 'banner': banner,
  356. 'cover': cover,
  357. 'poster': poster
  358. }
  359. return art
  360. def list_search(data):
  361. title = data['_links']['viaplay:search']['title']
  362. parameters = {'action': 'search', 'url': data['_links']['viaplay:search']['href']}
  363. add_item(title, parameters)
  364. def get_userinput(title):
  365. query = None
  366. keyboard = xbmc.Keyboard('', title)
  367. keyboard.doModal()
  368. if keyboard.isConfirmed():
  369. query = keyboard.getText()
  370. addon_log('User input string: %s' % query)
  371. return query
  372. def get_numeric_input(heading):
  373. dialog = xbmcgui.Dialog()
  374. numeric_input = dialog.numeric(0, heading)
  375. if len(numeric_input) > 0:
  376. return str(numeric_input)
  377. else:
  378. return None
  379. def search(url):
  380. try:
  381. query = get_userinput(language(30015))
  382. if len(query) > 0:
  383. url = '%s?query=%s' % (url, urllib.quote(query))
  384. list_products(url)
  385. except TypeError:
  386. pass
  387. def play_video(input, streamtype, content, pincode=None):
  388. if streamtype == 'url':
  389. url = input
  390. guid = vp.get_products(input=url, method='url')['system']['guid']
  391. else:
  392. guid = input
  393. try:
  394. video_urls = vp.get_video_urls(guid, pincode=pincode)
  395. if content == 'sport':
  396. # sports uses HLS v4 so we can't parse the manifest as audio is supplied externally
  397. stream_url = video_urls['manifest_url']
  398. else:
  399. bitrate = select_bitrate(video_urls['bitrates'].keys())
  400. if bitrate:
  401. stream_url = video_urls['bitrates'][bitrate]
  402. else:
  403. stream_url = False
  404. if stream_url:
  405. playitem = xbmcgui.ListItem(path=stream_url)
  406. playitem.setProperty('IsPlayable', 'true')
  407. if addon.getSetting('subtitles') == 'true':
  408. playitem.setSubtitles(vp.download_subtitles(video_urls['subtitle_urls']))
  409. xbmcplugin.setResolvedUrl(_handle, True, listitem=playitem)
  410. except vp.AuthFailure as error:
  411. if error.value == 'ParentalGuidancePinChallengeNeededError':
  412. if pincode:
  413. show_dialog(dialog_type='ok', heading=language(30033), message=language(30034))
  414. else:
  415. pincode = get_numeric_input(language(30032))
  416. if pincode:
  417. play_video(input, streamtype, content, pincode)
  418. else:
  419. show_auth_error(error.value)
  420. except vp.LoginFailure:
  421. show_dialog(dialog_type='ok', heading=language(30005), message=language(30006))
  422. def sports_menu(url):
  423. items = []
  424. event_date = ['today', 'upcoming', 'archive']
  425. for date in event_date:
  426. if date == 'today':
  427. title = language(30027)
  428. elif date == 'upcoming':
  429. title = language(30028)
  430. else:
  431. title = language(30029)
  432. if date == 'today':
  433. parameters = {'action': 'list_sports_today', 'url': url}
  434. else:
  435. parameters = {'action': 'list_sports_dates', 'url': url, 'event_date': date}
  436. items = add_item(title, parameters, items=items)
  437. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  438. xbmcplugin.endOfDirectory(_handle)
  439. def list_sports_today(url):
  440. items = []
  441. event_status = ['live', 'upcoming', 'archive']
  442. for status in event_status:
  443. if status == 'live':
  444. title = status.title()
  445. elif status == 'upcoming':
  446. title = language(30030)
  447. else:
  448. title = language(30031)
  449. parameters = {'action': 'list_products_sports_today', 'url': url, 'filter_sports_event': status}
  450. items = add_item(title, parameters, items=items)
  451. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  452. xbmcplugin.endOfDirectory(_handle)
  453. def list_sports_dates(url, event_date):
  454. items = []
  455. dates = vp.get_sports_dates(url, event_date)
  456. for date in dates:
  457. title = date['date']
  458. parameters = {'action': 'list_products', 'url': date['href']}
  459. items = add_item(title, parameters, items=items)
  460. xbmcplugin.addDirectoryItems(_handle, items, len(items))
  461. xbmcplugin.endOfDirectory(_handle)
  462. def ask_bitrate(bitrates):
  463. """Presents a dialog for user to select from a list of bitrates.
  464. Returns the value of the selected bitrate."""
  465. options = []
  466. for bitrate in bitrates:
  467. options.append(bitrate + ' Kbps')
  468. dialog = xbmcgui.Dialog()
  469. ret = dialog.select(language(30026), options)
  470. if ret > -1:
  471. return bitrates[ret]
  472. else:
  473. return None
  474. def select_bitrate(manifest_bitrates=None):
  475. """Returns a bitrate while honoring the user's preference."""
  476. bitrate_setting = int(addon.getSetting('preferred_bitrate'))
  477. if bitrate_setting == 0:
  478. preferred_bitrate = 'highest'
  479. elif bitrate_setting == 1:
  480. preferred_bitrate = 'limit'
  481. else:
  482. preferred_bitrate = 'ask'
  483. manifest_bitrates.sort(key=int, reverse=True)
  484. if preferred_bitrate == 'highest':
  485. return manifest_bitrates[0]
  486. elif preferred_bitrate == 'limit':
  487. allowed_bitrates = []
  488. max_bitrate_allowed = int(addon.getSetting('max_bitrate_allowed'))
  489. for bitrate in manifest_bitrates:
  490. if max_bitrate_allowed >= int(bitrate):
  491. allowed_bitrates.append(str(bitrate))
  492. if allowed_bitrates:
  493. return allowed_bitrates[0]
  494. else:
  495. return ask_bitrate(manifest_bitrates)
  496. def show_dialog(dialog_type, heading, message):
  497. dialog = xbmcgui.Dialog()
  498. if dialog_type == 'ok':
  499. dialog.ok(heading, message)
  500. def add_item(title, parameters, items=False, folder=True, playable=False, set_info=False, set_art=False,
  501. watched=False, set_content=False):
  502. listitem = xbmcgui.ListItem(label=title)
  503. if playable:
  504. listitem.setProperty('IsPlayable', 'true')
  505. folder = False
  506. if set_art:
  507. listitem.setArt(set_art)
  508. else:
  509. listitem.setArt({'icon': os.path.join(addon_path, 'icon.png')})
  510. listitem.setArt({'fanart': os.path.join(addon_path, 'fanart.jpg')})
  511. if set_info:
  512. listitem.setInfo('video', set_info)
  513. if not watched:
  514. listitem.addStreamInfo('video', {'duration': 0})
  515. if set_content:
  516. xbmcplugin.setContent(_handle, set_content)
  517. recursive_url = _url + '?' + urllib.urlencode(parameters)
  518. if items is False:
  519. xbmcplugin.addDirectoryItem(_handle, recursive_url, listitem, folder)
  520. else:
  521. items.append((recursive_url, listitem, folder))
  522. return items
  523. def router(paramstring):
  524. """Router function that calls other functions depending on the provided paramstring."""
  525. params = dict(urlparse.parse_qsl(paramstring))
  526. if params:
  527. if params['action'] == 'movies_menu':
  528. movies_menu(params['url'])
  529. elif params['action'] == 'kids_menu':
  530. kids_menu(params['url'])
  531. elif params['action'] == 'series_menu':
  532. series_menu(params['url'])
  533. elif params['action'] == 'sports_menu':
  534. sports_menu(params['url'])
  535. elif params['action'] == 'list_seasons':
  536. list_seasons(params['url'])
  537. elif params['action'] == 'list_products':
  538. list_products(params['url'])
  539. elif params['action'] == 'list_sports_today':
  540. list_sports_today(params['url'])
  541. elif params['action'] == 'list_products_sports_today':
  542. list_products(params['url'], params['filter_sports_event'])
  543. elif params['action'] == 'play_video':
  544. play_video(params['playid'], params['streamtype'], params['content'])
  545. elif params['action'] == 'list_sortings':
  546. list_sortings(params['url'])
  547. elif params['action'] == 'list_alphabetical_letters':
  548. list_alphabetical_letters(params['url'])
  549. elif params['action'] == 'search':
  550. search(params['url'])
  551. elif params['action'] == 'list_sports_dates':
  552. list_sports_dates(params['url'], params['event_date'])
  553. elif params['action'] == 'show_dialog':
  554. show_dialog(params['dialog_type'], params['heading'], params['message'])
  555. else:
  556. root_menu()
  557. if __name__ == '__main__':
  558. router(sys.argv[2][1:]) # trim the leading '?' from the plugin call paramstring