5b4528116fcb7d5a56f43775e8da757c32e92a08
[plugin.video.netflix.git] / resources / lib / KodiHelper.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # Module: KodiHelper
4 # Created on: 13.01.2017
5
6 import xbmcplugin
7 import xbmcgui
8 import xbmc
9 import json
10 from MSL import MSL
11 from os import remove
12 from os.path import join, isfile
13 from urllib import urlencode
14 from xbmcaddon import Addon
15 from uuid import uuid4
16 from utils import get_user_agent_for_current_platform
17 from UniversalAnalytics import Tracker
18 try:
19    import cPickle as pickle
20 except:
21    import pickle
22
23 class KodiHelper:
24     """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
25
26     def __init__ (self, plugin_handle=None, base_url=None):
27         """Fetches all needed info from Kodi & configures the baseline of the plugin
28
29         Parameters
30         ----------
31         plugin_handle : :obj:`int`
32             Plugin handle
33
34         base_url : :obj:`str`
35             Plugin base url
36         """
37         addon = self.get_addon()
38         self.plugin_handle = plugin_handle
39         self.base_url = base_url
40         self.plugin = addon.getAddonInfo('name')
41         self.version = addon.getAddonInfo('version')
42         self.base_data_path = xbmc.translatePath(addon.getAddonInfo('profile'))
43         self.home_path = xbmc.translatePath('special://home')
44         self.plugin_path = addon.getAddonInfo('path')
45         self.cookie_path = self.base_data_path + 'COOKIE'
46         self.data_path = self.base_data_path + 'DATA'
47         self.config_path = join(self.base_data_path, 'config')
48         self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
49         self.verb_log = addon.getSetting('logging') == 'true'
50         self.default_fanart = addon.getAddonInfo('fanart')
51         self.library = None
52         self.setup_memcache()
53
54     def get_addon (self):
55         """Returns a fresh addon instance"""
56         return Addon()
57
58     def refresh (self):
59         """Refresh the current list"""
60         return xbmc.executebuiltin('Container.Refresh')
61
62     def show_rating_dialog (self):
63         """Asks the user for a movie rating
64
65         Returns
66         -------
67         :obj:`int`
68             Movie rating between 0 & 10
69         """
70         dlg = xbmcgui.Dialog()
71         return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
72
73     def show_search_term_dialog (self):
74         """Asks the user for a term to query the netflix search for
75
76         Returns
77         -------
78         :obj:`str`
79             Term to search for
80         """
81         dlg = xbmcgui.Dialog()
82         term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
83         if len(term) == 0:
84             term = ' '
85         return term
86
87     def show_add_to_library_title_dialog (self, original_title):
88         """Asks the user for an alternative title for the show/movie that gets exported to the local library
89
90         Parameters
91         ----------
92         original_title : :obj:`str`
93             Original title of the show (as suggested by the addon)
94
95         Returns
96         -------
97         :obj:`str`
98             Title to persist
99         """
100         dlg = xbmcgui.Dialog()
101         return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
102
103     def show_password_dialog (self):
104         """Asks the user for its Netflix password
105
106         Returns
107         -------
108         :obj:`str`
109             Netflix password
110         """
111         dlg = xbmcgui.Dialog()
112         return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
113
114     def show_email_dialog (self):
115         """Asks the user for its Netflix account email
116
117         Returns
118         -------
119         term : :obj:`str`
120             Netflix account email
121         """
122         dlg = xbmcgui.Dialog()
123         return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
124
125     def show_login_failed_notification (self):
126         """Shows notification that the login failed
127
128         Returns
129         -------
130         bool
131             Dialog shown
132         """
133         dialog = xbmcgui.Dialog()
134         dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
135         return True
136
137     def show_missing_inputstream_addon_notification (self):
138         """Shows notification that the inputstream addon couldn't be found
139
140         Returns
141         -------
142         bool
143             Dialog shown
144         """
145         dialog = xbmcgui.Dialog()
146         dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
147         return True
148
149     def show_no_search_results_notification (self):
150         """Shows notification that no search results could be found
151
152         Returns
153         -------
154         bool
155             Dialog shown
156         """
157         dialog = xbmcgui.Dialog()
158         dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
159         return True
160
161     def show_no_seasons_notification (self):
162         """Shows notification that no seasons be found
163
164         Returns
165         -------
166         bool
167             Dialog shown
168         """
169         dialog = xbmcgui.Dialog()
170         dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
171         return True
172
173     def set_setting (self, key, value):
174         """Public interface for the addons setSetting method
175
176         Returns
177         -------
178         bool
179             Setting could be set or not
180         """
181         return self.get_addon().setSetting(key, value)
182
183     def get_credentials (self):
184         """Returns the users stored credentials
185
186         Returns
187         -------
188         :obj:`dict` of :obj:`str`
189             The users stored account data
190         """
191         return {
192             'email': self.get_addon().getSetting('email'),
193             'password': self.get_addon().getSetting('password')
194         }
195
196     def get_esn(self):
197         """
198         Returns the esn from settings
199         """
200         self.log(msg='Is FILE: ' + str(isfile(self.msl_data_path + 'msl_data.json')))
201         self.log(msg=self.get_addon().getSetting('esn'))
202         return self.get_addon().getSetting('esn')
203
204     def set_esn(self, esn):
205         """
206         Returns the esn from settings
207         """
208         stored_esn = self.get_esn()        
209         if not stored_esn and esn:
210             self.set_setting('esn', esn)
211             self.delete_manifest_data()            
212             return esn
213         return stored_esn
214     
215     def delete_manifest_data(self):
216         if isfile(self.msl_data_path + 'msl_data.json'):
217             remove(self.msl_data_path + 'msl_data.json')
218         if isfile(self.msl_data_path + 'manifest.json'):
219             remove(self.msl_data_path + 'manifest.json')
220         msl = MSL(kodi_helper=self)
221         msl.perform_key_handshake()
222         msl.save_msl_data()
223
224     def get_dolby_setting(self):
225         """
226         Returns if the dolby sound is enabled
227         :return: True|False
228         """
229         return self.get_addon().getSetting('enable_dolby_sound') == 'true'
230
231     def get_custom_library_settings (self):
232         """Returns the settings in regards to the custom library folder(s)
233
234         Returns
235         -------
236         :obj:`dict` of :obj:`str`
237             The users library settings
238         """
239         return {
240             'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
241             'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
242         }
243
244     def get_ssl_verification_setting (self):
245         """Returns the setting that describes if we should verify the ssl transport when loading data
246
247         Returns
248         -------
249         bool
250             Verify or not
251         """
252         return self.get_addon().getSetting('ssl_verification') == 'true'
253
254     def set_main_menu_selection (self, type):
255         """Persist the chosen main menu entry in memory
256
257         Parameters
258         ----------
259         type : :obj:`str`
260             Selected menu item
261         """
262         xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
263
264     def get_main_menu_selection (self):
265         """Gets the persisted chosen main menu entry from memory
266
267         Returns
268         -------
269         :obj:`str`
270             The last chosen main menu entry
271         """
272         return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
273
274     def setup_memcache (self):
275         """Sets up the memory cache if not existant"""
276         cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
277         # no cache setup yet, create one
278         if len(cached_items) < 1:
279             xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
280
281     def invalidate_memcache (self):
282         """Invalidates the memory cache"""
283         xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
284
285     def has_cached_item (self, cache_id):
286         """Checks if the requested item is in memory cache
287
288         Parameters
289         ----------
290         cache_id : :obj:`str`
291             ID of the cache entry
292
293         Returns
294         -------
295         bool
296             Item is cached
297         """
298         cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
299         return cache_id in cached_items.keys()
300
301     def get_cached_item (self, cache_id):
302         """Returns an item from the in memory cache
303
304         Parameters
305         ----------
306         cache_id : :obj:`str`
307             ID of the cache entry
308
309         Returns
310         -------
311         mixed
312             Contents of the requested cache item or none
313         """
314         cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
315         if self.has_cached_item(cache_id) != True:
316             return None
317         return cached_items[cache_id]
318
319     def add_cached_item (self, cache_id, contents):
320         """Adds an item to the in memory cache
321
322         Parameters
323         ----------
324         cache_id : :obj:`str`
325             ID of the cache entry
326
327         contents : mixed
328             Cache entry contents
329         """
330         cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
331         cached_items.update({cache_id: contents})
332         xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
333
334     def build_profiles_listing (self, profiles, action, build_url):
335         """Builds the profiles list Kodi screen
336
337         Parameters
338         ----------
339         profiles : :obj:`dict` of :obj:`str`
340             List of user profiles
341
342         action : :obj:`str`
343             Action paramter to build the subsequent routes
344
345         build_url : :obj:`fn`
346             Function to build the subsequent routes
347
348         Returns
349         -------
350         bool
351             List could be build
352         """
353         for profile_id in profiles:
354             profile = profiles[profile_id]
355             url = build_url({'action': action, 'profile_id': profile_id})
356             li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
357             li.setProperty('fanart_image', self.default_fanart)
358             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
359             xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
360         xbmcplugin.endOfDirectory(self.plugin_handle)
361         return True
362
363     def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
364         """Builds the video lists (my list, continue watching, etc.) Kodi screen
365
366         Parameters
367         ----------
368         video_list_ids : :obj:`dict` of :obj:`str`
369             List of video lists
370
371         user_list_order : :obj:`list` of :obj:`str`
372             Ordered user lists, to determine what should be displayed in the main menue
373
374         actions : :obj:`dict` of :obj:`str`
375             Dictionary of actions to build subsequent routes
376
377         build_url : :obj:`fn`
378             Function to build the subsequent routes
379
380         Returns
381         -------
382         bool
383             List could be build
384         """
385         preselect_items = []
386         for category in user_list_order:
387             for video_list_id in video_list_ids['user']:
388                 if video_list_ids['user'][video_list_id]['name'] == category:
389                     label = video_list_ids['user'][video_list_id]['displayName']
390                     if category == 'netflixOriginals':
391                         label = label.capitalize()
392                     li = xbmcgui.ListItem(label=label)
393                     li.setProperty('fanart_image', self.default_fanart)
394                     # determine action route
395                     action = actions['default']
396                     if category in actions.keys():
397                         action = actions[category]
398                     # determine if the item should be selected
399                     preselect_items.append((False, True)[category == self.get_main_menu_selection()])
400                     url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
401                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
402
403         # add recommendations/genres as subfolders (save us some space on the home page)
404         i18n_ids = {
405             'recommendations': self.get_local_string(30001),
406             'genres': self.get_local_string(30010)
407         }
408         for type in i18n_ids.keys():
409             # determine if the lists have contents
410             if len(video_list_ids[type]) > 0:
411                 # determine action route
412                 action = actions['default']
413                 if type in actions.keys():
414                     action = actions[type]
415                 # determine if the item should be selected
416                 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
417                 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
418                 li_rec.setProperty('fanart_image', self.default_fanart)
419                 url_rec = build_url({'action': action, 'type': type})
420                 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
421
422         # add search as subfolder
423         action = actions['default']
424         if 'search' in actions.keys():
425             action = actions[type]
426         li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
427         li_rec.setProperty('fanart_image', self.default_fanart)
428         url_rec = build_url({'action': action, 'type': 'search'})
429         xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
430
431         # no srting & close
432         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
433         xbmcplugin.endOfDirectory(self.plugin_handle)
434
435         # (re)select the previously selected main menu entry
436         idx = 1
437         for item in preselect_items:
438             idx += 1
439             preselected_list_item = idx if item else None
440         preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
441         if preselected_list_item != None:
442             xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
443         return True
444
445     def build_video_listing (self, video_list, actions, type, build_url):
446         """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
447
448         Parameters
449         ----------
450         video_list_ids : :obj:`dict` of :obj:`str`
451             List of video lists
452
453         actions : :obj:`dict` of :obj:`str`
454             Dictionary of actions to build subsequent routes
455
456         type : :obj:`str`
457             None or 'queue' f.e. when it´s a special video lists
458
459         build_url : :obj:`fn`
460             Function to build the subsequent routes
461
462         Returns
463         -------
464         bool
465             List could be build
466         """
467         for video_list_id in video_list:
468             video = video_list[video_list_id]
469             li = xbmcgui.ListItem(label=video['title'])
470             # add some art to the item
471             li = self._generate_art_info(entry=video, li=li)
472             # add list item info
473             li, infos = self._generate_entry_info(entry=video, li=li)
474             li = self._generate_context_menu_items(entry=video, li=li)
475             # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
476             if video_list[video_list_id]['type'] == 'movie':
477                 # it´s a movie, so we need no subfolder & a route to play it
478                 isFolder = False
479                 url = build_url({'action': 'play_video', 'video_id': video_list_id, 'infoLabels': infos})
480             else:
481                 # it´s a show, so we need a subfolder & route (for seasons)
482                 isFolder = True
483                 params = {'action': actions[video['type']], 'show_id': video_list_id}
484                 if 'tvshowtitle' in infos:
485                     params['tvshowtitle'] = infos['tvshowtitle']
486                 url = build_url(params)
487             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
488
489         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
490         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
491         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
492         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
493         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
494         xbmcplugin.endOfDirectory(self.plugin_handle)
495         return True
496
497     def build_search_result_listing (self, video_list, actions, build_url):
498         """Builds the search results list Kodi screen
499
500         Parameters
501         ----------
502         video_list : :obj:`dict` of :obj:`str`
503             List of videos or shows
504
505         actions : :obj:`dict` of :obj:`str`
506             Dictionary of actions to build subsequent routes
507
508         build_url : :obj:`fn`
509             Function to build the subsequent routes
510
511         Returns
512         -------
513         bool
514             List could be build
515         """
516         return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
517
518     def build_no_seasons_available (self):
519         """Builds the season list screen if no seasons could be found
520
521         Returns
522         -------
523         bool
524             List could be build
525         """
526         self.show_no_seasons_notification()
527         xbmcplugin.endOfDirectory(self.plugin_handle)
528         return True
529
530     def build_no_search_results_available (self, build_url, action):
531         """Builds the search results screen if no matches could be found
532
533         Parameters
534         ----------
535         action : :obj:`str`
536             Action paramter to build the subsequent routes
537
538         build_url : :obj:`fn`
539             Function to build the subsequent routes
540
541         Returns
542         -------
543         bool
544             List could be build
545         """
546         self.show_no_search_results_notification()
547         return xbmcplugin.endOfDirectory(self.plugin_handle)
548
549     def build_user_sub_listing (self, video_list_ids, type, action, build_url):
550         """Builds the video lists screen for user subfolders (genres & recommendations)
551
552         Parameters
553         ----------
554         video_list_ids : :obj:`dict` of :obj:`str`
555             List of video lists
556
557         type : :obj:`str`
558             List type (genre or recommendation)
559
560         action : :obj:`str`
561             Action paramter to build the subsequent routes
562
563         build_url : :obj:`fn`
564             Function to build the subsequent routes
565
566         Returns
567         -------
568         bool
569             List could be build
570         """
571         for video_list_id in video_list_ids:
572             li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
573             li.setProperty('fanart_image', self.default_fanart)
574             url = build_url({'action': action, 'video_list_id': video_list_id})
575             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
576
577         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
578         xbmcplugin.endOfDirectory(self.plugin_handle)
579         return True
580
581     def build_season_listing (self, seasons_sorted, season_list, build_url):
582         """Builds the season list screen for a show
583
584         Parameters
585         ----------
586         seasons_sorted : :obj:`list` of :obj:`str`
587             Sorted season indexes
588
589         season_list : :obj:`dict` of :obj:`str`
590             List of season entries
591
592         build_url : :obj:`fn`
593             Function to build the subsequent routes
594
595         Returns
596         -------
597         bool
598             List could be build
599         """
600         for index in seasons_sorted:
601             for season_id in season_list:
602                 season = season_list[season_id]
603                 if int(season['idx']) == index:
604                     li = xbmcgui.ListItem(label=season['text'])
605                     # add some art to the item
606                     li = self._generate_art_info(entry=season, li=li)
607                     # add list item info
608                     li, infos = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
609                     li = self._generate_context_menu_items(entry=season, li=li)
610                     params = {'action': 'episode_list', 'season_id': season_id}
611                     if 'tvshowtitle' in infos:
612                         params['tvshowtitle'] = infos['tvshowtitle']
613                     url = build_url(params)
614                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
615
616         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
617         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
618         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
619         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
620         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
621         xbmcplugin.endOfDirectory(self.plugin_handle)
622         return True
623
624     def build_episode_listing (self, episodes_sorted, episode_list, build_url):
625         """Builds the episode list screen for a season of a show
626
627         Parameters
628         ----------
629         episodes_sorted : :obj:`list` of :obj:`str`
630             Sorted episode indexes
631
632         episode_list : :obj:`dict` of :obj:`str`
633             List of episode entries
634
635         build_url : :obj:`fn`
636             Function to build the subsequent routes
637
638         Returns
639         -------
640         bool
641             List could be build
642         """
643         for index in episodes_sorted:
644             for episode_id in episode_list:
645                 episode = episode_list[episode_id]
646                 if int(episode['episode']) == index:
647                     li = xbmcgui.ListItem(label=episode['title'])
648                     # add some art to the item
649                     li = self._generate_art_info(entry=episode, li=li)
650                     # add list item info
651                     li, infos = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
652                     li = self._generate_context_menu_items(entry=episode, li=li)
653                     url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark'], 'infoLabels': infos})
654                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
655
656         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
657         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
658         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
659         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
660         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
661         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
662         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
663         xbmcplugin.endOfDirectory(self.plugin_handle)
664         return True
665
666     def play_item (self, esn, video_id, start_offset=-1, infoLabels={}):
667         """Plays a video
668
669         Parameters
670         ----------
671         esn : :obj:`str`
672             ESN needed for Widevine/Inputstream
673
674         video_id : :obj:`str`
675             ID of the video that should be played
676
677         start_offset : :obj:`str`
678             Offset to resume playback from (in seconds)
679         
680         infoLabels : :obj:`str`
681             the listitem's infoLabels
682
683         Returns
684         -------
685         bool
686             List could be build
687         """
688         self.set_esn(esn)
689         addon = self.get_addon()
690         inputstream_addon = self.get_inputstream_addon()
691         if inputstream_addon == None:
692             self.show_missing_inputstream_addon_notification()
693             self.log(msg='Inputstream addon not found')
694             return False
695
696         # track play event
697         self.track_event('playVideo')
698
699         # check esn in settings
700         settings_esn = str(addon.getSetting('esn'))
701         if len(settings_esn) == 0:
702             addon.setSetting('esn', str(esn))
703
704         # inputstream addon properties
705         msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
706         play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
707         play_item.setContentLookup(False)
708         play_item.setMimeType('application/dash+xml')
709         play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())        
710         play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
711         play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
712         play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
713         play_item.setProperty(inputstream_addon + '.server_certificate', 'Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eAGuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRITgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95OEtzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vfS6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0nu39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlGGCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqVDXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nAovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ0fFdCmw=')
714         play_item.setProperty('inputstreamaddon', inputstream_addon)
715
716         # check if we have a bookmark e.g. start offset position
717         if int(start_offset) > 0:
718             play_item.setProperty('StartOffset', str(start_offset) + '.0')
719         # set infoLabels
720         if len(infoLabels) > 0:
721             play_item.setInfo('video',  infoLabels)
722         return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
723
724     def _generate_art_info (self, entry, li):
725         """Adds the art info from an entry to a Kodi list item
726
727         Parameters
728         ----------
729         entry : :obj:`dict` of :obj:`str`
730             Entry that should be turned into a list item
731
732         li : :obj:`XMBC.ListItem`
733             Kodi list item instance
734
735         Returns
736         -------
737         :obj:`XMBC.ListItem`
738             Kodi list item instance
739         """
740         art = {'fanart': self.default_fanart}
741         if 'boxarts' in dict(entry).keys():
742             art.update({
743                 'poster': entry['boxarts']['big'],
744                 'landscape': entry['boxarts']['big'],
745                 'thumb': entry['boxarts']['small'],
746                 'fanart': entry['boxarts']['big']
747             })
748         if 'interesting_moment' in dict(entry).keys():
749             art.update({
750                 'poster': entry['interesting_moment'],
751                 'fanart': entry['interesting_moment']
752             })
753         if 'thumb' in dict(entry).keys():
754             art.update({'thumb': entry['thumb']})
755         if 'fanart' in dict(entry).keys():
756             art.update({'fanart': entry['fanart']})
757         if 'poster' in dict(entry).keys():
758             art.update({'poster': entry['poster']})
759         li.setArt(art)
760         return li
761
762     def _generate_entry_info (self, entry, li, base_info={}):
763         """Adds the item info from an entry to a Kodi list item
764
765         Parameters
766         ----------
767         entry : :obj:`dict` of :obj:`str`
768             Entry that should be turned into a list item
769
770         li : :obj:`XMBC.ListItem`
771             Kodi list item instance
772
773         base_info : :obj:`dict` of :obj:`str`
774             Additional info that overrules the entry info
775
776         Returns
777         -------
778         :obj:`XMBC.ListItem`
779             Kodi list item instance
780         """
781         infos = base_info
782         entry_keys = entry.keys()
783         if 'cast' in entry_keys and len(entry['cast']) > 0:
784             infos.update({'cast': entry['cast']})
785         if 'creators' in entry_keys and len(entry['creators']) > 0:
786             infos.update({'writer': entry['creators'][0]})
787         if 'directors' in entry_keys and len(entry['directors']) > 0:
788             infos.update({'director': entry['directors'][0]})
789         if 'genres' in entry_keys and len(entry['genres']) > 0:
790             infos.update({'genre': entry['genres'][0]})
791         if 'maturity' in entry_keys:
792             if 'mpaa' in entry_keys:
793                 infos.update({'mpaa': entry['mpaa']})
794             else:
795                 if entry.get('maturity', None) is not None:
796                     if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
797                         infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
798         if 'rating' in entry_keys:
799             infos.update({'rating': int(entry['rating']) * 2})
800         if 'synopsis' in entry_keys:
801             infos.update({'plot': entry['synopsis']})
802         if 'plot' in entry_keys:
803             infos.update({'plot': entry['plot']})
804         if 'runtime' in entry_keys:
805             infos.update({'duration': entry['runtime']})
806         if 'duration' in entry_keys:
807             infos.update({'duration': entry['duration']})
808         if 'seasons_label' in entry_keys:
809             infos.update({'season': entry['seasons_label']})
810         if 'season' in entry_keys:
811             infos.update({'season': entry['season']})
812         if 'title' in entry_keys:
813             infos.update({'title': entry['title']})
814         if 'type' in entry_keys:
815             if entry['type'] == 'movie' or entry['type'] == 'episode':
816                 li.setProperty('IsPlayable', 'true')
817             elif entry['type'] == 'show':
818                 infos.update({'tvshowtitle': entry['title']})
819         if 'mediatype' in entry_keys:
820             if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
821                 li.setProperty('IsPlayable', 'true')
822                 infos.update({'mediatype': entry['mediatype']})
823         if 'watched' in entry_keys:
824             infos.update({'playcount': (1, 0)[entry['watched']]})
825         if 'index' in entry_keys:
826             infos.update({'episode': entry['index']})
827         if 'episode' in entry_keys:
828             infos.update({'episode': entry['episode']})
829         if 'year' in entry_keys:
830             infos.update({'year': entry['year']})
831         if 'quality' in entry_keys:
832             quality = {'width': '960', 'height': '540'}
833             if entry['quality'] == '720':
834                 quality = {'width': '1280', 'height': '720'}
835             if entry['quality'] == '1080':
836                 quality = {'width': '1920', 'height': '1080'}
837             li.addStreamInfo('video', quality)
838         if 'tvshowtitle' in entry_keys:
839             infos.update({'tvshowtitle': entry['tvshowtitle']})
840         li.setInfo('video', infos)
841         return li, infos
842
843     def _generate_context_menu_items (self, entry, li):
844         """Adds context menue items to a Kodi list item
845
846         Parameters
847         ----------
848         entry : :obj:`dict` of :obj:`str`
849             Entry that should be turned into a list item
850
851         li : :obj:`XMBC.ListItem`
852             Kodi list item instance
853         Returns
854         -------
855         :obj:`XMBC.ListItem`
856             Kodi list item instance
857         """
858         items = []
859         action = {}
860         entry_keys = entry.keys()
861
862         # action item templates
863         encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
864         url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
865         actions = [
866             ['export_to_library', self.get_local_string(30018), 'export'],
867             ['remove_from_library', self.get_local_string(30030), 'remove'],
868             ['rate_on_netflix', self.get_local_string(30019), 'rating'],
869             ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
870             ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
871         ]
872
873         # build concrete action items
874         for action_item in actions:
875             action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
876
877         # add or remove the movie/show/season/episode from & to the users "My List"
878         if 'in_my_list' in entry_keys:
879             items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
880         elif 'queue' in entry_keys:
881             items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
882         elif 'my_list' in entry_keys:
883             items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
884         # rate the movie/show/season/episode on Netflix
885         items.append(action['rate_on_netflix'])
886
887         # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
888         if 'type' in entry_keys:
889             # add/remove movie
890             if entry['type'] == 'movie':
891                 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
892                 items.append(action[action_type])
893             # add/remove show
894             if entry['type'] == 'show' and 'title' in entry_keys:
895                 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
896                 items.append(action[action_type])
897
898         # add it to the item
899         li.addContextMenuItems(items)
900         return li
901
902     def log (self, msg, level=xbmc.LOGDEBUG):
903         """Adds a log entry to the Kodi log
904
905         Parameters
906         ----------
907         msg : :obj:`str`
908             Entry that should be turned into a list item
909
910         level : :obj:`int`
911             Kodi log level
912         """
913         if isinstance(msg, unicode):
914             msg = msg.encode('utf-8')
915         xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
916
917     def get_local_string (self, string_id):
918         """Returns the localized version of a string
919
920         Parameters
921         ----------
922         string_id : :obj:`int`
923             ID of the string that shoudl be fetched
924
925         Returns
926         -------
927         :obj:`str`
928             Requested string or empty string
929         """
930         src = xbmc if string_id < 30000 else self.get_addon()
931         locString = src.getLocalizedString(string_id)
932         if isinstance(locString, unicode):
933             locString = locString.encode('utf-8')
934         return locString
935
936     def get_inputstream_addon (self):
937         """Checks if the inputstream addon is installed & enabled.
938            Returns the type of the inputstream addon used or None if not found
939
940         Returns
941         -------
942         :obj:`str` or None
943             Inputstream addon or None
944         """
945         type = 'inputstream.adaptive'
946         payload = {
947             'jsonrpc': '2.0',
948             'id': 1,
949             'method': 'Addons.GetAddonDetails',
950             'params': {
951                 'addonid': type,
952                 'properties': ['enabled']
953             }
954         }
955         response = xbmc.executeJSONRPC(json.dumps(payload))
956         data = json.loads(response)
957         if not 'error' in data.keys():
958             if data['result']['addon']['enabled'] == True:
959                 return type
960         return None
961
962     def set_library (self, library):
963         """Adds an instance of the Library class
964
965         Parameters
966         ----------
967         library : :obj:`Library`
968             instance of the Library class
969         """
970         self.library = library
971
972     def track_event(self, event):
973         """
974         Send a tracking event if tracking is enabled
975         :param event: the string idetifier of the event
976         :return: None
977         """
978         addon = self.get_addon()
979         # Check if tracking is enabled
980         enable_tracking = (addon.getSetting('enable_tracking') == 'true')
981         if enable_tracking:
982             #Get or Create Tracking id
983             tracking_id = addon.getSetting('tracking_id')
984             if tracking_id is '':
985                 tracking_id = str(uuid4())
986                 addon.setSetting('tracking_id', tracking_id)
987             # Send the tracking event
988             tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
989             tracker.send('event', event)