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