7e1811339ed923cedf094679763af94a451571a9
[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             # it´s a show, so we need a subfolder & route (for seasons)
473             isFolder = True
474             url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
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})
480             # add list item info
481             li = self._generate_entry_info(entry=video, li=li)
482             li = self._generate_context_menu_items(entry=video, li=li)
483             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
484
485         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
486         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
487         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
488         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
489         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
490         xbmcplugin.endOfDirectory(self.plugin_handle)
491         return True
492
493     def build_search_result_listing (self, video_list, actions, build_url):
494         """Builds the search results list Kodi screen
495
496         Parameters
497         ----------
498         video_list : :obj:`dict` of :obj:`str`
499             List of videos or shows
500
501         actions : :obj:`dict` of :obj:`str`
502             Dictionary of actions to build subsequent routes
503
504         build_url : :obj:`fn`
505             Function to build the subsequent routes
506
507         Returns
508         -------
509         bool
510             List could be build
511         """
512         return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
513
514     def build_no_seasons_available (self):
515         """Builds the season list screen if no seasons could be found
516
517         Returns
518         -------
519         bool
520             List could be build
521         """
522         self.show_no_seasons_notification()
523         xbmcplugin.endOfDirectory(self.plugin_handle)
524         return True
525
526     def build_no_search_results_available (self, build_url, action):
527         """Builds the search results screen if no matches could be found
528
529         Parameters
530         ----------
531         action : :obj:`str`
532             Action paramter to build the subsequent routes
533
534         build_url : :obj:`fn`
535             Function to build the subsequent routes
536
537         Returns
538         -------
539         bool
540             List could be build
541         """
542         self.show_no_search_results_notification()
543         return xbmcplugin.endOfDirectory(self.plugin_handle)
544
545     def build_user_sub_listing (self, video_list_ids, type, action, build_url):
546         """Builds the video lists screen for user subfolders (genres & recommendations)
547
548         Parameters
549         ----------
550         video_list_ids : :obj:`dict` of :obj:`str`
551             List of video lists
552
553         type : :obj:`str`
554             List type (genre or recommendation)
555
556         action : :obj:`str`
557             Action paramter to build the subsequent routes
558
559         build_url : :obj:`fn`
560             Function to build the subsequent routes
561
562         Returns
563         -------
564         bool
565             List could be build
566         """
567         for video_list_id in video_list_ids:
568             li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
569             li.setProperty('fanart_image', self.default_fanart)
570             url = build_url({'action': action, 'video_list_id': video_list_id})
571             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
572
573         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
574         xbmcplugin.endOfDirectory(self.plugin_handle)
575         return True
576
577     def build_season_listing (self, seasons_sorted, season_list, build_url):
578         """Builds the season list screen for a show
579
580         Parameters
581         ----------
582         seasons_sorted : :obj:`list` of :obj:`str`
583             Sorted season indexes
584
585         season_list : :obj:`dict` of :obj:`str`
586             List of season entries
587
588         build_url : :obj:`fn`
589             Function to build the subsequent routes
590
591         Returns
592         -------
593         bool
594             List could be build
595         """
596         for index in seasons_sorted:
597             for season_id in season_list:
598                 season = season_list[season_id]
599                 if int(season['idx']) == index:
600                     li = xbmcgui.ListItem(label=season['text'])
601                     # add some art to the item
602                     li = self._generate_art_info(entry=season, li=li)
603                     # add list item info
604                     li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
605                     li = self._generate_context_menu_items(entry=season, li=li)
606                     url = build_url({'action': 'episode_list', 'season_id': season_id})
607                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
608
609         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
610         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
611         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
612         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
613         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
614         xbmcplugin.endOfDirectory(self.plugin_handle)
615         return True
616
617     def build_episode_listing (self, episodes_sorted, episode_list, build_url):
618         """Builds the episode list screen for a season of a show
619
620         Parameters
621         ----------
622         episodes_sorted : :obj:`list` of :obj:`str`
623             Sorted episode indexes
624
625         episode_list : :obj:`dict` of :obj:`str`
626             List of episode entries
627
628         build_url : :obj:`fn`
629             Function to build the subsequent routes
630
631         Returns
632         -------
633         bool
634             List could be build
635         """
636         for index in episodes_sorted:
637             for episode_id in episode_list:
638                 episode = episode_list[episode_id]
639                 if int(episode['episode']) == index:
640                     li = xbmcgui.ListItem(label=episode['title'])
641                     # add some art to the item
642                     li = self._generate_art_info(entry=episode, li=li)
643                     # add list item info
644                     li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
645                     li = self._generate_context_menu_items(entry=episode, li=li)
646                     url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
647                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
648
649         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
650         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
651         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
652         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
653         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
654         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
655         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
656         xbmcplugin.endOfDirectory(self.plugin_handle)
657         return True
658
659     def play_item (self, esn, video_id, start_offset=-1):
660         """Plays a video
661
662         Parameters
663         ----------
664         esn : :obj:`str`
665             ESN needed for Widevine/Inputstream
666
667         video_id : :obj:`str`
668             ID of the video that should be played
669
670         start_offset : :obj:`str`
671             Offset to resume playback from (in seconds)
672
673         Returns
674         -------
675         bool
676             List could be build
677         """
678         self.set_esn(esn)
679         addon = self.get_addon()
680         inputstream_addon = self.get_inputstream_addon()
681         if inputstream_addon == None:
682             self.show_missing_inputstream_addon_notification()
683             self.log(msg='Inputstream addon not found')
684             return False
685
686         # track play event
687         self.track_event('playVideo')
688
689         # check esn in settings
690         settings_esn = str(addon.getSetting('esn'))
691         if len(settings_esn) == 0:
692             addon.setSetting('esn', str(esn))
693
694         # inputstream addon properties
695         msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
696         play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
697         play_item.setContentLookup(False)
698         play_item.setMimeType('application/dash+xml')
699         play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())        
700         play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
701         play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
702         play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
703         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=')
704         play_item.setProperty('inputstreamaddon', inputstream_addon)
705
706         # check if we have a bookmark e.g. start offset position
707         if int(start_offset) > 0:
708             play_item.setProperty('StartOffset', str(start_offset) + '.0')
709         return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
710
711     def _generate_art_info (self, entry, li):
712         """Adds the art info from an entry to a Kodi list item
713
714         Parameters
715         ----------
716         entry : :obj:`dict` of :obj:`str`
717             Entry that should be turned into a list item
718
719         li : :obj:`XMBC.ListItem`
720             Kodi list item instance
721
722         Returns
723         -------
724         :obj:`XMBC.ListItem`
725             Kodi list item instance
726         """
727         art = {'fanart': self.default_fanart}
728         if 'boxarts' in dict(entry).keys():
729             art.update({
730                 'poster': entry['boxarts']['big'],
731                 'landscape': entry['boxarts']['big'],
732                 'thumb': entry['boxarts']['small'],
733                 'fanart': entry['boxarts']['big']
734             })
735         if 'interesting_moment' in dict(entry).keys():
736             art.update({
737                 'poster': entry['interesting_moment'],
738                 'fanart': entry['interesting_moment']
739             })
740         if 'thumb' in dict(entry).keys():
741             art.update({'thumb': entry['thumb']})
742         if 'fanart' in dict(entry).keys():
743             art.update({'fanart': entry['fanart']})
744         if 'poster' in dict(entry).keys():
745             art.update({'poster': entry['poster']})
746         li.setArt(art)
747         return li
748
749     def _generate_entry_info (self, entry, li, base_info={}):
750         """Adds the item info from an entry to a Kodi list item
751
752         Parameters
753         ----------
754         entry : :obj:`dict` of :obj:`str`
755             Entry that should be turned into a list item
756
757         li : :obj:`XMBC.ListItem`
758             Kodi list item instance
759
760         base_info : :obj:`dict` of :obj:`str`
761             Additional info that overrules the entry info
762
763         Returns
764         -------
765         :obj:`XMBC.ListItem`
766             Kodi list item instance
767         """
768         infos = base_info
769         entry_keys = entry.keys()
770         if 'cast' in entry_keys and len(entry['cast']) > 0:
771             infos.update({'cast': entry['cast']})
772         if 'creators' in entry_keys and len(entry['creators']) > 0:
773             infos.update({'writer': entry['creators'][0]})
774         if 'directors' in entry_keys and len(entry['directors']) > 0:
775             infos.update({'director': entry['directors'][0]})
776         if 'genres' in entry_keys and len(entry['genres']) > 0:
777             infos.update({'genre': entry['genres'][0]})
778         if 'maturity' in entry_keys:
779             if 'mpaa' in entry_keys:
780                 infos.update({'mpaa': entry['mpaa']})
781             else:
782                 if entry.get('maturity', None) is not None:
783                     if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
784                         infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
785         if 'rating' in entry_keys:
786             infos.update({'rating': int(entry['rating']) * 2})
787         if 'synopsis' in entry_keys:
788             infos.update({'plot': entry['synopsis']})
789         if 'plot' in entry_keys:
790             infos.update({'plot': entry['plot']})
791         if 'runtime' in entry_keys:
792             infos.update({'duration': entry['runtime']})
793         if 'duration' in entry_keys:
794             infos.update({'duration': entry['duration']})
795         if 'seasons_label' in entry_keys:
796             infos.update({'season': entry['seasons_label']})
797         if 'season' in entry_keys:
798             infos.update({'season': entry['season']})
799         if 'title' in entry_keys:
800             infos.update({'title': entry['title']})
801         if 'type' in entry_keys:
802             if entry['type'] == 'movie' or entry['type'] == 'episode':
803                 li.setProperty('IsPlayable', 'true')
804         if 'mediatype' in entry_keys:
805             if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
806                 li.setProperty('IsPlayable', 'true')
807                 infos.update({'mediatype': entry['mediatype']})
808         if 'watched' in entry_keys:
809             infos.update({'playcount': (1, 0)[entry['watched']]})
810         if 'index' in entry_keys:
811             infos.update({'episode': entry['index']})
812         if 'episode' in entry_keys:
813             infos.update({'episode': entry['episode']})
814         if 'year' in entry_keys:
815             infos.update({'year': entry['year']})
816         if 'quality' in entry_keys:
817             quality = {'width': '960', 'height': '540'}
818             if entry['quality'] == '720':
819                 quality = {'width': '1280', 'height': '720'}
820             if entry['quality'] == '1080':
821                 quality = {'width': '1920', 'height': '1080'}
822             li.addStreamInfo('video', quality)
823         li.setInfo('video', infos)
824         return li
825
826     def _generate_context_menu_items (self, entry, li):
827         """Adds context menue items to a Kodi list item
828
829         Parameters
830         ----------
831         entry : :obj:`dict` of :obj:`str`
832             Entry that should be turned into a list item
833
834         li : :obj:`XMBC.ListItem`
835             Kodi list item instance
836         Returns
837         -------
838         :obj:`XMBC.ListItem`
839             Kodi list item instance
840         """
841         items = []
842         action = {}
843         entry_keys = entry.keys()
844
845         # action item templates
846         encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
847         url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
848         actions = [
849             ['export_to_library', self.get_local_string(30018), 'export'],
850             ['remove_from_library', self.get_local_string(30030), 'remove'],
851             ['rate_on_netflix', self.get_local_string(30019), 'rating'],
852             ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
853             ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
854         ]
855
856         # build concrete action items
857         for action_item in actions:
858             action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
859
860         # add or remove the movie/show/season/episode from & to the users "My List"
861         if 'in_my_list' in entry_keys:
862             items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
863         elif 'queue' in entry_keys:
864             items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
865         elif 'my_list' in entry_keys:
866             items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
867         # rate the movie/show/season/episode on Netflix
868         items.append(action['rate_on_netflix'])
869
870         # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
871         if 'type' in entry_keys:
872             # add/remove movie
873             if entry['type'] == 'movie':
874                 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
875                 items.append(action[action_type])
876             # add/remove show
877             if entry['type'] == 'show' and 'title' in entry_keys:
878                 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
879                 items.append(action[action_type])
880
881         # add it to the item
882         li.addContextMenuItems(items)
883         return li
884
885     def log (self, msg, level=xbmc.LOGDEBUG):
886         """Adds a log entry to the Kodi log
887
888         Parameters
889         ----------
890         msg : :obj:`str`
891             Entry that should be turned into a list item
892
893         level : :obj:`int`
894             Kodi log level
895         """
896         if isinstance(msg, unicode):
897             msg = msg.encode('utf-8')
898         xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
899
900     def get_local_string (self, string_id):
901         """Returns the localized version of a string
902
903         Parameters
904         ----------
905         string_id : :obj:`int`
906             ID of the string that shoudl be fetched
907
908         Returns
909         -------
910         :obj:`str`
911             Requested string or empty string
912         """
913         src = xbmc if string_id < 30000 else self.get_addon()
914         locString = src.getLocalizedString(string_id)
915         if isinstance(locString, unicode):
916             locString = locString.encode('utf-8')
917         return locString
918
919     def get_inputstream_addon (self):
920         """Checks if the inputstream addon is installed & enabled.
921            Returns the type of the inputstream addon used or None if not found
922
923         Returns
924         -------
925         :obj:`str` or None
926             Inputstream addon or None
927         """
928         type = 'inputstream.adaptive'
929         payload = {
930             'jsonrpc': '2.0',
931             'id': 1,
932             'method': 'Addons.GetAddonDetails',
933             'params': {
934                 'addonid': type,
935                 'properties': ['enabled']
936             }
937         }
938         response = xbmc.executeJSONRPC(json.dumps(payload))
939         data = json.loads(response)
940         if not 'error' in data.keys():
941             if data['result']['addon']['enabled'] == True:
942                 return type
943         return None
944
945     def set_library (self, library):
946         """Adds an instance of the Library class
947
948         Parameters
949         ----------
950         library : :obj:`Library`
951             instance of the Library class
952         """
953         self.library = library
954
955     def track_event(self, event):
956         """
957         Send a tracking event if tracking is enabled
958         :param event: the string idetifier of the event
959         :return: None
960         """
961         addon = self.get_addon()
962         # Check if tracking is enabled
963         enable_tracking = (addon.getSetting('enable_tracking') == 'true')
964         if enable_tracking:
965             #Get or Create Tracking id
966             tracking_id = addon.getSetting('tracking_id')
967             if tracking_id is '':
968                 tracking_id = str(uuid4())
969                 addon.setSetting('tracking_id', tracking_id)
970             # Send the tracking event
971             tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
972             tracker.send('event', event)