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