9fdb8bff9584a28e238807365c5e8c31465dbe4c
[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 os
7 import urllib
8 import xbmcplugin
9 import xbmcgui
10 import xbmcaddon
11 import xbmc
12 import json
13 import uuid
14 from UniversalAnalytics import Tracker
15 try:
16    import cPickle as pickle
17 except:
18    import pickle
19
20 class KodiHelper:
21     """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
22
23     def __init__ (self, plugin_handle, base_url):
24         """Fetches all needed info from Kodi & configures the baseline of the plugin
25
26         Parameters
27         ----------
28         plugin_handle : :obj:`int`
29             Plugin handle
30
31         base_url : :obj:`str`
32             Plugin base url
33         """
34         self.plugin_handle = plugin_handle
35         self.base_url = base_url
36         self.addon = xbmcaddon.Addon()
37         self.plugin = self.addon.getAddonInfo('name')
38         self.base_data_path = xbmc.translatePath(self.addon.getAddonInfo('profile'))
39         self.home_path = xbmc.translatePath('special://home')
40         self.plugin_path = self.addon.getAddonInfo('path')
41         self.cookie_path = self.base_data_path + 'COOKIE'
42         self.data_path = self.base_data_path + 'DATA'
43         self.config_path = os.path.join(self.base_data_path, 'config')
44         self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
45         self.verb_log = self.addon.getSetting('logging') == 'true'
46         self.default_fanart = self.addon.getAddonInfo('fanart')
47         self.win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
48         self.library = None
49         self.setup_memcache()
50
51     def refresh (self):
52         """Refrsh the current list"""
53         return xbmc.executebuiltin('Container.Refresh')
54
55     def show_rating_dialog (self):
56         """Asks the user for a movie rating
57
58         Returns
59         -------
60         :obj:`int`
61             Movie rating between 0 & 10
62         """
63         dlg = xbmcgui.Dialog()
64         return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
65
66     def show_search_term_dialog (self):
67         """Asks the user for a term to query the netflix search for
68
69         Returns
70         -------
71         :obj:`str`
72             Term to search for
73         """
74         dlg = xbmcgui.Dialog()
75         term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
76         if len(term) == 0:
77             term = ' '
78         return term
79
80     def show_add_to_library_title_dialog (self, original_title):
81         """Asks the user for an alternative title for the show/movie that gets exported to the local library
82
83         Parameters
84         ----------
85         original_title : :obj:`str`
86             Original title of the show (as suggested by the addon)
87
88         Returns
89         -------
90         :obj:`str`
91             Title to persist
92         """
93         dlg = xbmcgui.Dialog()
94         return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
95
96     def show_password_dialog (self):
97         """Asks the user for its Netflix password
98
99         Returns
100         -------
101         :obj:`str`
102             Netflix password
103         """
104         dlg = xbmcgui.Dialog()
105         return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
106
107     def show_email_dialog (self):
108         """Asks the user for its Netflix account email
109
110         Returns
111         -------
112         term : :obj:`str`
113             Netflix account email
114         """
115         dlg = xbmcgui.Dialog()
116         return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
117
118     def show_login_failed_notification (self):
119         """Shows notification that the login failed
120
121         Returns
122         -------
123         bool
124             Dialog shown
125         """
126         dialog = xbmcgui.Dialog()
127         dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
128         return True
129
130     def show_missing_inputstream_addon_notification (self):
131         """Shows notification that the inputstream addon couldn't be found
132
133         Returns
134         -------
135         bool
136             Dialog shown
137         """
138         dialog = xbmcgui.Dialog()
139         dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
140         return True
141
142     def show_no_search_results_notification (self):
143         """Shows notification that no search results could be found
144
145         Returns
146         -------
147         bool
148             Dialog shown
149         """
150         dialog = xbmcgui.Dialog()
151         dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
152         return True
153
154     def show_no_seasons_notification (self):
155         """Shows notification that no seasons be found
156
157         Returns
158         -------
159         bool
160             Dialog shown
161         """
162         dialog = xbmcgui.Dialog()
163         dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
164         return True
165
166     def set_setting (self, key, value):
167         """Public interface for the addons setSetting method
168
169         Returns
170         -------
171         bool
172             Setting could be set or not
173         """
174         return self.addon.setSetting(key, value)
175
176     def get_credentials (self):
177         """Returns the users stored credentials
178
179         Returns
180         -------
181         :obj:`dict` of :obj:`str`
182             The users stored account data
183         """
184         return {
185             'email': self.addon.getSetting('email'),
186             'password': self.addon.getSetting('password')
187         }
188
189     def get_custom_library_settings (self):
190         """Returns the settings in regards to the custom library folder(s)
191
192         Returns
193         -------
194         :obj:`dict` of :obj:`str`
195             The users library settings
196         """
197         return {
198             'enablelibraryfolder': self.addon.getSetting('enablelibraryfolder'),
199             'customlibraryfolder': self.addon.getSetting('customlibraryfolder')
200         }
201
202     def get_ssl_verification_setting (self):
203         """Returns the setting that describes if we should verify the ssl transport when loading data
204
205         Returns
206         -------
207         bool
208             Verify or not
209         """
210         return self.addon.getSetting('ssl_verification') == 'true'
211
212     def set_main_menu_selection (self, type):
213         """Persist the chosen main menu entry in memory
214
215         Parameters
216         ----------
217         type : :obj:`str`
218             Selected menu item
219         """
220         self.win.setProperty('main_menu_selection', type)
221
222     def get_main_menu_selection (self):
223         """Gets the persisted chosen main menu entry from memory
224
225         Returns
226         -------
227         :obj:`str`
228             The last chosen main menu entry
229         """
230         return self.win.getProperty('main_menu_selection')
231
232     def setup_memcache (self):
233         """Sets up the memory cache if not existant"""
234         cached_items = self.win.getProperty('memcache')
235         # no cache setup yet, create one
236         if len(cached_items) < 1:
237             self.win.setProperty('memcache', pickle.dumps({}))
238
239     def invalidate_memcache (self):
240         """Invalidates the memory cache"""
241         self.win.setProperty('memcache', pickle.dumps({}))
242
243     def has_cached_item (self, cache_id):
244         """Checks if the requested item is in memory cache
245
246         Parameters
247         ----------
248         cache_id : :obj:`str`
249             ID of the cache entry
250
251         Returns
252         -------
253         bool
254             Item is cached
255         """
256         cached_items = pickle.loads(self.win.getProperty('memcache'))
257         return cache_id in cached_items.keys()
258
259     def get_cached_item (self, cache_id):
260         """Returns an item from the in memory cache
261
262         Parameters
263         ----------
264         cache_id : :obj:`str`
265             ID of the cache entry
266
267         Returns
268         -------
269         mixed
270             Contents of the requested cache item or none
271         """
272         cached_items = pickle.loads(self.win.getProperty('memcache'))
273         if self.has_cached_item(cache_id) != True:
274             return None
275         return cached_items[cache_id]
276
277     def add_cached_item (self, cache_id, contents):
278         """Adds an item to the in memory cache
279
280         Parameters
281         ----------
282         cache_id : :obj:`str`
283             ID of the cache entry
284
285         contents : mixed
286             Cache entry contents
287         """
288         cached_items = pickle.loads(self.win.getProperty('memcache'))
289         cached_items.update({cache_id: contents})
290         self.win.setProperty('memcache', pickle.dumps(cached_items))
291
292     def build_profiles_listing (self, profiles, action, build_url):
293         """Builds the profiles list Kodi screen
294
295         Parameters
296         ----------
297         profiles : :obj:`dict` of :obj:`str`
298             List of user profiles
299
300         action : :obj:`str`
301             Action paramter to build the subsequent routes
302
303         build_url : :obj:`fn`
304             Function to build the subsequent routes
305
306         Returns
307         -------
308         bool
309             List could be build
310         """
311         for profile_id in profiles:
312             profile = profiles[profile_id]
313             url = build_url({'action': action, 'profile_id': profile_id})
314             li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
315             li.setProperty('fanart_image', self.default_fanart)
316             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
317             xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
318         xbmcplugin.endOfDirectory(self.plugin_handle)
319         return True
320
321     def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
322         """Builds the video lists (my list, continue watching, etc.) Kodi screen
323
324         Parameters
325         ----------
326         video_list_ids : :obj:`dict` of :obj:`str`
327             List of video lists
328
329         user_list_order : :obj:`list` of :obj:`str`
330             Ordered user lists, to determine what should be displayed in the main menue
331
332         actions : :obj:`dict` of :obj:`str`
333             Dictionary of actions to build subsequent routes
334
335         build_url : :obj:`fn`
336             Function to build the subsequent routes
337
338         Returns
339         -------
340         bool
341             List could be build
342         """
343         preselect_items = []
344         for category in user_list_order:
345             for video_list_id in video_list_ids['user']:
346                 if video_list_ids['user'][video_list_id]['name'] == category:
347                     label = video_list_ids['user'][video_list_id]['displayName']
348                     if category == 'netflixOriginals':
349                         label = label.capitalize()
350                     li = xbmcgui.ListItem(label=label)
351                     li.setProperty('fanart_image', self.default_fanart)
352                     # determine action route
353                     action = actions['default']
354                     if category in actions.keys():
355                         action = actions[category]
356                     # determine if the item should be selected
357                     preselect_items.append((False, True)[category == self.get_main_menu_selection()])
358                     url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
359                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
360
361         # add recommendations/genres as subfolders (save us some space on the home page)
362         i18n_ids = {
363             'recommendations': self.get_local_string(30001),
364             'genres': self.get_local_string(30010)
365         }
366         for type in i18n_ids.keys():
367             # determine if the lists have contents
368             if len(video_list_ids[type]) > 0:
369                 # determine action route
370                 action = actions['default']
371                 if type in actions.keys():
372                     action = actions[type]
373                 # determine if the item should be selected
374                 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
375                 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
376                 li_rec.setProperty('fanart_image', self.default_fanart)
377                 url_rec = build_url({'action': action, 'type': type})
378                 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
379
380         # add search as subfolder
381         action = actions['default']
382         if 'search' in actions.keys():
383             action = actions[type]
384         li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
385         li_rec.setProperty('fanart_image', self.default_fanart)
386         url_rec = build_url({'action': action, 'type': 'search'})
387         xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
388
389         # no srting & close
390         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
391         xbmcplugin.endOfDirectory(self.plugin_handle)
392
393         # (re)select the previously selected main menu entry
394         idx = 1
395         for item in preselect_items:
396             idx += 1
397             preselected_list_item = idx if item else None
398         preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
399         if preselected_list_item != None:
400             xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(self.win.getFocusId()), str(preselected_list_item)))
401         return True
402
403     def build_video_listing (self, video_list, actions, type, build_url):
404         """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
405
406         Parameters
407         ----------
408         video_list_ids : :obj:`dict` of :obj:`str`
409             List of video lists
410
411         actions : :obj:`dict` of :obj:`str`
412             Dictionary of actions to build subsequent routes
413
414         type : :obj:`str`
415             None or 'queue' f.e. when it´s a special video lists
416
417         build_url : :obj:`fn`
418             Function to build the subsequent routes
419
420         Returns
421         -------
422         bool
423             List could be build
424         """
425         for video_list_id in video_list:
426             video = video_list[video_list_id]
427             if type != 'queue' or (type == 'queue' and video['in_my_list'] == True):
428                 li = xbmcgui.ListItem(label=video['title'])
429                 # add some art to the item
430                 li = self._generate_art_info(entry=video, li=li)
431                 # it´s a show, so we need a subfolder & route (for seasons)
432                 isFolder = True
433                 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
434                 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
435                 if video_list[video_list_id]['type'] == 'movie':
436                     # it´s a movie, so we need no subfolder & a route to play it
437                     isFolder = False
438                     url = build_url({'action': 'play_video', 'video_id': video_list_id})
439                 # add list item info
440                 li = self._generate_entry_info(entry=video, li=li)
441                 li = self._generate_context_menu_items(entry=video, li=li)
442                 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
443
444         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
445         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
446         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
447         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
448         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
449         xbmcplugin.endOfDirectory(self.plugin_handle)
450         return True
451
452     def build_search_result_listing (self, video_list, actions, build_url):
453         """Builds the search results list Kodi screen
454
455         Parameters
456         ----------
457         video_list : :obj:`dict` of :obj:`str`
458             List of videos or shows
459
460         actions : :obj:`dict` of :obj:`str`
461             Dictionary of actions to build subsequent routes
462
463         build_url : :obj:`fn`
464             Function to build the subsequent routes
465
466         Returns
467         -------
468         bool
469             List could be build
470         """
471         return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
472
473     def build_no_seasons_available (self):
474         """Builds the season list screen if no seasons could be found
475
476         Returns
477         -------
478         bool
479             List could be build
480         """
481         self.show_no_seasons_notification()
482         xbmcplugin.endOfDirectory(self.plugin_handle)
483         return True
484
485     def build_no_search_results_available (self, build_url, action):
486         """Builds the search results screen if no matches could be found
487
488         Parameters
489         ----------
490         action : :obj:`str`
491             Action paramter to build the subsequent routes
492
493         build_url : :obj:`fn`
494             Function to build the subsequent routes
495
496         Returns
497         -------
498         bool
499             List could be build
500         """
501         self.show_no_search_results_notification()
502         return xbmcplugin.endOfDirectory(self.plugin_handle)
503
504     def build_user_sub_listing (self, video_list_ids, type, action, build_url):
505         """Builds the video lists screen for user subfolders (genres & recommendations)
506
507         Parameters
508         ----------
509         video_list_ids : :obj:`dict` of :obj:`str`
510             List of video lists
511
512         type : :obj:`str`
513             List type (genre or recommendation)
514
515         action : :obj:`str`
516             Action paramter to build the subsequent routes
517
518         build_url : :obj:`fn`
519             Function to build the subsequent routes
520
521         Returns
522         -------
523         bool
524             List could be build
525         """
526         for video_list_id in video_list_ids:
527             li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
528             li.setProperty('fanart_image', self.default_fanart)
529             url = build_url({'action': action, 'video_list_id': video_list_id})
530             xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
531
532         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
533         xbmcplugin.endOfDirectory(self.plugin_handle)
534         return True
535
536     def build_season_listing (self, seasons_sorted, season_list, build_url):
537         """Builds the season list screen for a show
538
539         Parameters
540         ----------
541         seasons_sorted : :obj:`list` of :obj:`str`
542             Sorted season indexes
543
544         season_list : :obj:`dict` of :obj:`str`
545             List of season entries
546
547         build_url : :obj:`fn`
548             Function to build the subsequent routes
549
550         Returns
551         -------
552         bool
553             List could be build
554         """
555         for index in seasons_sorted:
556             for season_id in season_list:
557                 season = season_list[season_id]
558                 if int(season['idx']) == index:
559                     li = xbmcgui.ListItem(label=season['text'])
560                     # add some art to the item
561                     li = self._generate_art_info(entry=season, li=li)
562                     # add list item info
563                     li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
564                     li = self._generate_context_menu_items(entry=season, li=li)
565                     url = build_url({'action': 'episode_list', 'season_id': season_id})
566                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
567
568         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
569         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
570         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
571         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
572         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
573         xbmcplugin.endOfDirectory(self.plugin_handle)
574         return True
575
576     def build_episode_listing (self, episodes_sorted, episode_list, build_url):
577         """Builds the episode list screen for a season of a show
578
579         Parameters
580         ----------
581         episodes_sorted : :obj:`list` of :obj:`str`
582             Sorted episode indexes
583
584         episode_list : :obj:`dict` of :obj:`str`
585             List of episode entries
586
587         build_url : :obj:`fn`
588             Function to build the subsequent routes
589
590         Returns
591         -------
592         bool
593             List could be build
594         """
595         for index in episodes_sorted:
596             for episode_id in episode_list:
597                 episode = episode_list[episode_id]
598                 if int(episode['episode']) == index:
599                     li = xbmcgui.ListItem(label=episode['title'])
600                     # add some art to the item
601                     li = self._generate_art_info(entry=episode, li=li)
602                     # add list item info
603                     li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
604                     li = self._generate_context_menu_items(entry=episode, li=li)
605                     url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
606                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
607
608         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
609         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
610         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
611         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
612         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
613         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
614         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
615         xbmcplugin.endOfDirectory(self.plugin_handle)
616         return True
617
618     def play_item (self, esn, video_id, start_offset=-1):
619         """Plays a video
620
621         Parameters
622         ----------
623         esn : :obj:`str`
624             ESN needed for Widevine/Inputstream
625
626         video_id : :obj:`str`
627             ID of the video that should be played
628
629         start_offset : :obj:`str`
630             Offset to resume playback from (in seconds)
631
632         Returns
633         -------
634         bool
635             List could be build
636         """
637         inputstream_addon = self.get_inputstream_addon()
638         if inputstream_addon == None:
639             self.show_missing_inputstream_addon_notification()
640             self.log(msg='Inputstream addon not found')
641             return False
642
643         # track play event
644         self.track_event('playVideo')
645
646         # inputstream addon properties
647         msl_service_url = 'http://localhost:' + str(self.addon.getSetting('msl_service_port'))
648         play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
649         play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
650         play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
651         play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
652         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=')
653         # TODO: Change when Kodi can handle/trnsfer defaults in hidden values in settings
654         #play_item.setProperty(inputstream_addon + '.server_certificate', self.addon.getSetting('msl_service_certificate'))
655         play_item.setProperty('inputstreamaddon', inputstream_addon)
656
657         # check if we have a bookmark e.g. start offset position
658         if int(start_offset) > 0:
659             play_item.setProperty('StartOffset', str(start_offset) + '.0')
660         return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
661
662     def _generate_art_info (self, entry, li):
663         """Adds the art info from an entry to a Kodi list item
664
665         Parameters
666         ----------
667         entry : :obj:`dict` of :obj:`str`
668             Entry that should be turned into a list item
669
670         li : :obj:`XMBC.ListItem`
671             Kodi list item instance
672
673         Returns
674         -------
675         :obj:`XMBC.ListItem`
676             Kodi list item instance
677         """
678         art = {'fanart': self.default_fanart}
679         if 'boxarts' in dict(entry).keys():
680             art.update({
681                 'poster': entry['boxarts']['big'],
682                 'landscape': entry['boxarts']['big'],
683                 'thumb': entry['boxarts']['small'],
684                 'fanart': entry['boxarts']['big']
685             })
686         if 'interesting_moment' in dict(entry).keys():
687             art.update({
688                 'poster': entry['interesting_moment'],
689                 'fanart': entry['interesting_moment']
690             })
691         if 'thumb' in dict(entry).keys():
692             art.update({'thumb': entry['thumb']})
693         if 'fanart' in dict(entry).keys():
694             art.update({'fanart': entry['fanart']})
695         if 'poster' in dict(entry).keys():
696             art.update({'poster': entry['poster']})
697         li.setArt(art)
698         return li
699
700     def _generate_entry_info (self, entry, li, base_info={}):
701         """Adds the item info from an entry to a Kodi list item
702
703         Parameters
704         ----------
705         entry : :obj:`dict` of :obj:`str`
706             Entry that should be turned into a list item
707
708         li : :obj:`XMBC.ListItem`
709             Kodi list item instance
710
711         base_info : :obj:`dict` of :obj:`str`
712             Additional info that overrules the entry info
713
714         Returns
715         -------
716         :obj:`XMBC.ListItem`
717             Kodi list item instance
718         """
719         infos = base_info
720         entry_keys = entry.keys()
721         if 'cast' in entry_keys and len(entry['cast']) > 0:
722             infos.update({'cast': entry['cast']})
723         if 'creators' in entry_keys and len(entry['creators']) > 0:
724             infos.update({'writer': entry['creators'][0]})
725         if 'directors' in entry_keys and len(entry['directors']) > 0:
726             infos.update({'director': entry['directors'][0]})
727         if 'genres' in entry_keys and len(entry['genres']) > 0:
728             infos.update({'genre': entry['genres'][0]})
729         if 'maturity' in entry_keys:
730             if 'mpaa' in entry_keys:
731                 infos.update({'mpaa': entry['mpaa']})
732             else:
733                 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
734         if 'rating' in entry_keys:
735             infos.update({'rating': int(entry['rating']) * 2})
736         if 'synopsis' in entry_keys:
737             infos.update({'plot': entry['synopsis']})
738         if 'plot' in entry_keys:
739             infos.update({'plot': entry['plot']})
740         if 'runtime' in entry_keys:
741             infos.update({'duration': entry['runtime']})
742         if 'duration' in entry_keys:
743             infos.update({'duration': entry['duration']})
744         if 'seasons_label' in entry_keys:
745             infos.update({'season': entry['seasons_label']})
746         if 'season' in entry_keys:
747             infos.update({'season': entry['season']})
748         if 'title' in entry_keys:
749             infos.update({'title': entry['title']})
750         if 'type' in entry_keys:
751             if entry['type'] == 'movie' or entry['type'] == 'episode':
752                 li.setProperty('IsPlayable', 'true')
753         if 'mediatype' in entry_keys:
754             if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
755                 li.setProperty('IsPlayable', 'true')
756                 infos.update({'mediatype': entry['mediatype']})
757         if 'watched' in entry_keys:
758             infos.update({'playcount': (1, 0)[entry['watched']]})
759         if 'index' in entry_keys:
760             infos.update({'episode': entry['index']})
761         if 'episode' in entry_keys:
762             infos.update({'episode': entry['episode']})
763         if 'year' in entry_keys:
764             infos.update({'year': entry['year']})
765         if 'quality' in entry_keys:
766             quality = {'width': '960', 'height': '540'}
767             if entry['quality'] == '720':
768                 quality = {'width': '1280', 'height': '720'}
769             if entry['quality'] == '1080':
770                 quality = {'width': '1920', 'height': '1080'}
771             li.addStreamInfo('video', quality)
772         li.setInfo('video', infos)
773         return li
774
775     def _generate_context_menu_items (self, entry, li):
776         """Adds context menue items to a Kodi list item
777
778         Parameters
779         ----------
780         entry : :obj:`dict` of :obj:`str`
781             Entry that should be turned into a list item
782
783         li : :obj:`XMBC.ListItem`
784             Kodi list item instance
785         Returns
786         -------
787         :obj:`XMBC.ListItem`
788             Kodi list item instance
789         """
790         items = []
791         action = {}
792         entry_keys = entry.keys()
793
794         # action item templates
795         encoded_title = urllib.urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
796         url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
797         actions = [
798             ['export_to_library', self.get_local_string(30018), 'export'],
799             ['remove_from_library', self.get_local_string(30030), 'remove'],
800             ['rate_on_netflix', self.get_local_string(30019), 'rating'],
801             ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
802             ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
803         ]
804
805         # build concrete action items
806         for action_item in actions:
807             action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
808
809         # add or remove the movie/show/season/episode from & to the users "My List"
810         if 'in_my_list' in entry_keys:
811             items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
812         elif 'queue' in entry_keys:
813             items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
814         elif 'my_list' in entry_keys:
815             items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
816         # rate the movie/show/season/episode on Netflix
817         items.append(action['rate_on_netflix'])
818
819         # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
820         if 'type' in entry_keys:
821             # add/remove movie
822             if entry['type'] == 'movie':
823                 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
824                 items.append(action[action_type])
825             # add/remove show
826             if entry['type'] == 'show' and 'title' in entry_keys:
827                 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
828                 items.append(action[action_type])
829
830         # add it to the item
831         li.addContextMenuItems(items)
832         return li
833
834     def log (self, msg, level=xbmc.LOGDEBUG):
835         """Adds a log entry to the Kodi log
836
837         Parameters
838         ----------
839         msg : :obj:`str`
840             Entry that should be turned into a list item
841
842         level : :obj:`int`
843             Kodi log level
844         """
845         if isinstance(msg, unicode):
846             msg = msg.encode('utf-8')
847         xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
848
849     def get_local_string (self, string_id):
850         """Returns the localized version of a string
851
852         Parameters
853         ----------
854         string_id : :obj:`int`
855             ID of the string that shoudl be fetched
856
857         Returns
858         -------
859         :obj:`str`
860             Requested string or empty string
861         """
862         src = xbmc if string_id < 30000 else self.addon
863         locString = src.getLocalizedString(string_id)
864         if isinstance(locString, unicode):
865             locString = locString.encode('utf-8')
866         return locString
867
868     def get_inputstream_addon (self):
869         """Checks if the inputstream addon is installed & enabled.
870            Returns the type of the inputstream addon used or None if not found
871
872         Returns
873         -------
874         :obj:`str` or None
875             Inputstream addon or None
876         """
877         type = 'inputstream.adaptive'
878         payload = {
879             'jsonrpc': '2.0',
880             'id': 1,
881             'method': 'Addons.GetAddonDetails',
882             'params': {
883                 'addonid': type,
884                 'properties': ['enabled']
885             }
886         }
887         response = xbmc.executeJSONRPC(json.dumps(payload))
888         data = json.loads(response)
889         if not 'error' in data.keys():
890             if data['result']['addon']['enabled'] == True:
891                 return type
892         return None
893
894     def set_library (self, library):
895         """Adds an instance of the Library class
896
897         Parameters
898         ----------
899         library : :obj:`Library`
900             instance of the Library class
901         """
902         self.library = library
903
904     def track_event(self, event):
905         """
906         Send a tracking event if tracking is enabled
907         :param event: the string idetifier of the event
908         :return: None
909         """
910         # Check if tracking is enabled
911         enable_tracking = (self.addon.getSetting('enable_logging') == 'true')
912         if enable_tracking:
913             #Get or Create Tracking id
914             tracking_id = self.addon.getSetting('tracking_id')
915             if tracking_id is '':
916                 tracking_id = str(uuid.uuid4())
917                 self.addon.setSetting('tracking_id', tracking_id)
918             # Send the tracking event
919             tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
920             tracker.send('event', event)