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