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