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