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