Tweak use of in memory cache.
[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 get_cached_item (self, cache_id):
286         """Returns an item from the in memory cache
287
288         Parameters
289         ----------
290         cache_id : :obj:`str`
291             ID of the cache entry
292
293         Returns
294         -------
295         mixed
296             Contents of the requested cache item or none
297         """
298         cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
299
300         return cached_items.get(cache_id)
301
302     def add_cached_item (self, cache_id, contents):
303         """Adds an item to the in memory cache
304
305         Parameters
306         ----------
307         cache_id : :obj:`str`
308             ID of the cache entry
309
310         contents : mixed
311             Cache entry contents
312         """
313         cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
314         cached_items.update({cache_id: contents})
315         xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
316
317     def build_profiles_listing (self, profiles, action, build_url):
318         """Builds the profiles list Kodi screen
319
320         Parameters
321         ----------
322         profiles : :obj:`dict` of :obj:`str`
323             List of user profiles
324
325         action : :obj:`str`
326             Action paramter to build the subsequent routes
327
328         build_url : :obj:`fn`
329             Function to build the subsequent routes
330
331         Returns
332         -------
333         bool
334             List could be build
335         """
336         for profile_id in profiles:
337             profile = profiles[profile_id]
338             url = build_url({'action': action, 'profile_id': profile_id})
339             li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
340             li.setProperty('fanart_image', self.default_fanart)
341             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
342             xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
343         xbmcplugin.endOfDirectory(self.plugin_handle)
344         return True
345
346     def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
347         """Builds the video lists (my list, continue watching, etc.) Kodi screen
348
349         Parameters
350         ----------
351         video_list_ids : :obj:`dict` of :obj:`str`
352             List of video lists
353
354         user_list_order : :obj:`list` of :obj:`str`
355             Ordered user lists, to determine what should be displayed in the main menue
356
357         actions : :obj:`dict` of :obj:`str`
358             Dictionary of actions to build subsequent routes
359
360         build_url : :obj:`fn`
361             Function to build the subsequent routes
362
363         Returns
364         -------
365         bool
366             List could be build
367         """
368         preselect_items = []
369         for category in user_list_order:
370             for video_list_id in video_list_ids['user']:
371                 if video_list_ids['user'][video_list_id]['name'] == category:
372                     label = video_list_ids['user'][video_list_id]['displayName']
373                     if category == 'netflixOriginals':
374                         label = label.capitalize()
375                     li = xbmcgui.ListItem(label=label)
376                     li.setProperty('fanart_image', self.default_fanart)
377                     # determine action route
378                     action = actions['default']
379                     if category in actions.keys():
380                         action = actions[category]
381                     # determine if the item should be selected
382                     preselect_items.append((False, True)[category == self.get_main_menu_selection()])
383                     url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
384                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
385
386         # add recommendations/genres as subfolders (save us some space on the home page)
387         i18n_ids = {
388             'recommendations': self.get_local_string(30001),
389             'genres': self.get_local_string(30010)
390         }
391         for type in i18n_ids.keys():
392             # determine if the lists have contents
393             if len(video_list_ids[type]) > 0:
394                 # determine action route
395                 action = actions['default']
396                 if type in actions.keys():
397                     action = actions[type]
398                 # determine if the item should be selected
399                 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
400                 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
401                 li_rec.setProperty('fanart_image', self.default_fanart)
402                 url_rec = build_url({'action': action, 'type': type})
403                 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
404
405         # add search as subfolder
406         action = actions['default']
407         if 'search' in actions.keys():
408             action = actions[type]
409         li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
410         li_rec.setProperty('fanart_image', self.default_fanart)
411         url_rec = build_url({'action': action, 'type': 'search'})
412         xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
413
414         # no srting & close
415         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
416         xbmcplugin.endOfDirectory(self.plugin_handle)
417
418         # (re)select the previously selected main menu entry
419         idx = 1
420         for item in preselect_items:
421             idx += 1
422             preselected_list_item = idx if item else None
423         preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
424         if preselected_list_item != None:
425             xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
426         return True
427
428     def build_video_listing (self, video_list, actions, type, build_url):
429         """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
430
431         Parameters
432         ----------
433         video_list_ids : :obj:`dict` of :obj:`str`
434             List of video lists
435
436         actions : :obj:`dict` of :obj:`str`
437             Dictionary of actions to build subsequent routes
438
439         type : :obj:`str`
440             None or 'queue' f.e. when it´s a special video lists
441
442         build_url : :obj:`fn`
443             Function to build the subsequent routes
444
445         Returns
446         -------
447         bool
448             List could be build
449         """
450         for video_list_id in video_list:
451             video = video_list[video_list_id]
452             li = xbmcgui.ListItem(label=video['title'])
453             # add some art to the item
454             li = self._generate_art_info(entry=video, li=li)
455             # add list item info
456             li, infos = self._generate_entry_info(entry=video, li=li)
457             li = self._generate_context_menu_items(entry=video, li=li)
458             # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
459             if video_list[video_list_id]['type'] == 'movie':
460                 # it´s a movie, so we need no subfolder & a route to play it
461                 isFolder = False
462                 url = build_url({'action': 'play_video', 'video_id': video_list_id, 'infoLabels': infos})
463             else:
464                 # it´s a show, so we need a subfolder & route (for seasons)
465                 isFolder = True
466                 params = {'action': actions[video['type']], 'show_id': video_list_id}
467                 if 'tvshowtitle' in infos:
468                     params['tvshowtitle'] = infos['tvshowtitle']
469                 url = build_url(params)
470             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
471
472         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
473         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
474         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
475         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
476         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
477         xbmcplugin.endOfDirectory(self.plugin_handle)
478         return True
479
480     def build_search_result_listing (self, video_list, actions, build_url):
481         """Builds the search results list Kodi screen
482
483         Parameters
484         ----------
485         video_list : :obj:`dict` of :obj:`str`
486             List of videos or shows
487
488         actions : :obj:`dict` of :obj:`str`
489             Dictionary of actions to build subsequent routes
490
491         build_url : :obj:`fn`
492             Function to build the subsequent routes
493
494         Returns
495         -------
496         bool
497             List could be build
498         """
499         return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
500
501     def build_no_seasons_available (self):
502         """Builds the season list screen if no seasons could be found
503
504         Returns
505         -------
506         bool
507             List could be build
508         """
509         self.show_no_seasons_notification()
510         xbmcplugin.endOfDirectory(self.plugin_handle)
511         return True
512
513     def build_no_search_results_available (self, build_url, action):
514         """Builds the search results screen if no matches could be found
515
516         Parameters
517         ----------
518         action : :obj:`str`
519             Action paramter to build the subsequent routes
520
521         build_url : :obj:`fn`
522             Function to build the subsequent routes
523
524         Returns
525         -------
526         bool
527             List could be build
528         """
529         self.show_no_search_results_notification()
530         return xbmcplugin.endOfDirectory(self.plugin_handle)
531
532     def build_user_sub_listing (self, video_list_ids, type, action, build_url):
533         """Builds the video lists screen for user subfolders (genres & recommendations)
534
535         Parameters
536         ----------
537         video_list_ids : :obj:`dict` of :obj:`str`
538             List of video lists
539
540         type : :obj:`str`
541             List type (genre or recommendation)
542
543         action : :obj:`str`
544             Action paramter to build the subsequent routes
545
546         build_url : :obj:`fn`
547             Function to build the subsequent routes
548
549         Returns
550         -------
551         bool
552             List could be build
553         """
554         for video_list_id in video_list_ids:
555             li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
556             li.setProperty('fanart_image', self.default_fanart)
557             url = build_url({'action': action, 'video_list_id': video_list_id})
558             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
559
560         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
561         xbmcplugin.endOfDirectory(self.plugin_handle)
562         return True
563
564     def build_season_listing (self, seasons_sorted, season_list, build_url):
565         """Builds the season list screen for a show
566
567         Parameters
568         ----------
569         seasons_sorted : :obj:`list` of :obj:`str`
570             Sorted season indexes
571
572         season_list : :obj:`dict` of :obj:`str`
573             List of season entries
574
575         build_url : :obj:`fn`
576             Function to build the subsequent routes
577
578         Returns
579         -------
580         bool
581             List could be build
582         """
583         for index in seasons_sorted:
584             for season_id in season_list:
585                 season = season_list[season_id]
586                 if int(season['idx']) == index:
587                     li = xbmcgui.ListItem(label=season['text'])
588                     # add some art to the item
589                     li = self._generate_art_info(entry=season, li=li)
590                     # add list item info
591                     li, infos = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
592                     li = self._generate_context_menu_items(entry=season, li=li)
593                     params = {'action': 'episode_list', 'season_id': season_id}
594                     if 'tvshowtitle' in infos:
595                         params['tvshowtitle'] = infos['tvshowtitle']
596                     url = build_url(params)
597                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
598
599         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
600         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
601         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
602         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
603         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
604         xbmcplugin.endOfDirectory(self.plugin_handle)
605         return True
606
607     def build_episode_listing (self, episodes_sorted, episode_list, build_url):
608         """Builds the episode list screen for a season of a show
609
610         Parameters
611         ----------
612         episodes_sorted : :obj:`list` of :obj:`str`
613             Sorted episode indexes
614
615         episode_list : :obj:`dict` of :obj:`str`
616             List of episode entries
617
618         build_url : :obj:`fn`
619             Function to build the subsequent routes
620
621         Returns
622         -------
623         bool
624             List could be build
625         """
626         for index in episodes_sorted:
627             for episode_id in episode_list:
628                 episode = episode_list[episode_id]
629                 if int(episode['episode']) == index:
630                     li = xbmcgui.ListItem(label=episode['title'])
631                     # add some art to the item
632                     li = self._generate_art_info(entry=episode, li=li)
633                     # add list item info
634                     li, infos = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
635                     li = self._generate_context_menu_items(entry=episode, li=li)
636                     url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark'], 'infoLabels': infos})
637                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
638
639         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
640         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
641         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
642         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
643         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
644         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
645         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
646         xbmcplugin.endOfDirectory(self.plugin_handle)
647         return True
648
649     def play_item (self, esn, video_id, start_offset=-1, infoLabels={}):
650         """Plays a video
651
652         Parameters
653         ----------
654         esn : :obj:`str`
655             ESN needed for Widevine/Inputstream
656
657         video_id : :obj:`str`
658             ID of the video that should be played
659
660         start_offset : :obj:`str`
661             Offset to resume playback from (in seconds)
662         
663         infoLabels : :obj:`str`
664             the listitem's infoLabels
665
666         Returns
667         -------
668         bool
669             List could be build
670         """
671         self.set_esn(esn)
672         addon = self.get_addon()
673         inputstream_addon = self.get_inputstream_addon()
674         if inputstream_addon == None:
675             self.show_missing_inputstream_addon_notification()
676             self.log(msg='Inputstream addon not found')
677             return False
678
679         # track play event
680         self.track_event('playVideo')
681
682         # check esn in settings
683         settings_esn = str(addon.getSetting('esn'))
684         if len(settings_esn) == 0:
685             addon.setSetting('esn', str(esn))
686
687         # inputstream addon properties
688         msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
689         play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
690         play_item.setContentLookup(False)
691         play_item.setMimeType('application/dash+xml')
692         play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())        
693         play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
694         play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
695         play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
696         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=')
697         play_item.setProperty('inputstreamaddon', inputstream_addon)
698
699         # check if we have a bookmark e.g. start offset position
700         if int(start_offset) > 0:
701             play_item.setProperty('StartOffset', str(start_offset) + '.0')
702         # set infoLabels
703         if len(infoLabels) > 0:
704             play_item.setInfo('video',  infoLabels)
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             elif entry['type'] == 'show':
801                 infos.update({'tvshowtitle': entry['title']})
802         if 'mediatype' in entry_keys:
803             if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
804                 li.setProperty('IsPlayable', 'true')
805                 infos.update({'mediatype': entry['mediatype']})
806         if 'watched' in entry_keys:
807             infos.update({'playcount': (1, 0)[entry['watched']]})
808         if 'index' in entry_keys:
809             infos.update({'episode': entry['index']})
810         if 'episode' in entry_keys:
811             infos.update({'episode': entry['episode']})
812         if 'year' in entry_keys:
813             infos.update({'year': entry['year']})
814         if 'quality' in entry_keys:
815             quality = {'width': '960', 'height': '540'}
816             if entry['quality'] == '720':
817                 quality = {'width': '1280', 'height': '720'}
818             if entry['quality'] == '1080':
819                 quality = {'width': '1920', 'height': '1080'}
820             li.addStreamInfo('video', quality)
821         if 'tvshowtitle' in entry_keys:
822             infos.update({'tvshowtitle': entry['tvshowtitle']})
823         li.setInfo('video', infos)
824         return li, infos
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)