2 # -*- coding: utf-8 -*-
4 # Created on: 13.01.2017
10 from os.path import join
11 from urllib import urlencode
12 from xbmcaddon import Addon
13 from uuid import uuid4
14 from UniversalAnalytics import Tracker
16 import cPickle as pickle
21 """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
23 def __init__ (self, plugin_handle=None, base_url=None):
24 """Fetches all needed info from Kodi & configures the baseline of the plugin
28 plugin_handle : :obj:`int`
34 self.plugin_handle = plugin_handle
35 self.base_url = base_url
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 = 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')
51 """Refresh the current list"""
52 return xbmc.executebuiltin('Container.Refresh')
54 def show_rating_dialog (self):
55 """Asks the user for a movie rating
60 Movie rating between 0 & 10
62 dlg = xbmcgui.Dialog()
63 return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
65 def show_search_term_dialog (self):
66 """Asks the user for a term to query the netflix search for
73 dlg = xbmcgui.Dialog()
74 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
79 def show_add_to_library_title_dialog (self, original_title):
80 """Asks the user for an alternative title for the show/movie that gets exported to the local library
84 original_title : :obj:`str`
85 Original title of the show (as suggested by the addon)
92 dlg = xbmcgui.Dialog()
93 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
95 def show_password_dialog (self):
96 """Asks the user for its Netflix password
103 dlg = xbmcgui.Dialog()
104 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
106 def show_email_dialog (self):
107 """Asks the user for its Netflix account email
112 Netflix account email
114 dlg = xbmcgui.Dialog()
115 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
117 def show_login_failed_notification (self):
118 """Shows notification that the login failed
125 dialog = xbmcgui.Dialog()
126 dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
129 def show_missing_inputstream_addon_notification (self):
130 """Shows notification that the inputstream addon couldn't be found
137 dialog = xbmcgui.Dialog()
138 dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
141 def show_no_search_results_notification (self):
142 """Shows notification that no search results could be found
149 dialog = xbmcgui.Dialog()
150 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
153 def show_no_seasons_notification (self):
154 """Shows notification that no seasons be found
161 dialog = xbmcgui.Dialog()
162 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
165 def set_setting (self, key, value):
166 """Public interface for the addons setSetting method
171 Setting could be set or not
173 return self.addon.setSetting(key, value)
175 def get_credentials (self):
176 """Returns the users stored credentials
180 :obj:`dict` of :obj:`str`
181 The users stored account data
184 'email': self.addon.getSetting('email'),
185 'password': self.addon.getSetting('password')
188 def get_dolby_setting(self):
190 Returns if the dolby sound is enabled
193 return self.addon.getSetting('enable_dolby_sound') == 'true'
195 def get_custom_library_settings (self):
196 """Returns the settings in regards to the custom library folder(s)
200 :obj:`dict` of :obj:`str`
201 The users library settings
204 'enablelibraryfolder': self.addon.getSetting('enablelibraryfolder'),
205 'customlibraryfolder': self.addon.getSetting('customlibraryfolder')
208 def get_ssl_verification_setting (self):
209 """Returns the setting that describes if we should verify the ssl transport when loading data
216 return self.addon.getSetting('ssl_verification') == 'true'
218 def set_main_menu_selection (self, type):
219 """Persist the chosen main menu entry in memory
226 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
228 def get_main_menu_selection (self):
229 """Gets the persisted chosen main menu entry from memory
234 The last chosen main menu entry
236 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
238 def setup_memcache (self):
239 """Sets up the memory cache if not existant"""
240 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
241 # no cache setup yet, create one
242 if len(cached_items) < 1:
243 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
245 def invalidate_memcache (self):
246 """Invalidates the memory cache"""
247 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
249 def has_cached_item (self, cache_id):
250 """Checks if the requested item is in memory cache
254 cache_id : :obj:`str`
255 ID of the cache entry
262 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
263 return cache_id in cached_items.keys()
265 def get_cached_item (self, cache_id):
266 """Returns an item from the in memory cache
270 cache_id : :obj:`str`
271 ID of the cache entry
276 Contents of the requested cache item or none
278 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
279 if self.has_cached_item(cache_id) != True:
281 return cached_items[cache_id]
283 def add_cached_item (self, cache_id, contents):
284 """Adds an item to the in memory cache
288 cache_id : :obj:`str`
289 ID of the cache entry
294 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
295 cached_items.update({cache_id: contents})
296 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
298 def build_profiles_listing (self, profiles, action, build_url):
299 """Builds the profiles list Kodi screen
303 profiles : :obj:`dict` of :obj:`str`
304 List of user profiles
307 Action paramter to build the subsequent routes
309 build_url : :obj:`fn`
310 Function to build the subsequent routes
317 for profile_id in profiles:
318 profile = profiles[profile_id]
319 url = build_url({'action': action, 'profile_id': profile_id})
320 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
321 li.setProperty('fanart_image', self.default_fanart)
322 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
323 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
324 xbmcplugin.endOfDirectory(self.plugin_handle)
327 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
328 """Builds the video lists (my list, continue watching, etc.) Kodi screen
332 video_list_ids : :obj:`dict` of :obj:`str`
335 user_list_order : :obj:`list` of :obj:`str`
336 Ordered user lists, to determine what should be displayed in the main menue
338 actions : :obj:`dict` of :obj:`str`
339 Dictionary of actions to build subsequent routes
341 build_url : :obj:`fn`
342 Function to build the subsequent routes
350 for category in user_list_order:
351 for video_list_id in video_list_ids['user']:
352 if video_list_ids['user'][video_list_id]['name'] == category:
353 label = video_list_ids['user'][video_list_id]['displayName']
354 if category == 'netflixOriginals':
355 label = label.capitalize()
356 li = xbmcgui.ListItem(label=label)
357 li.setProperty('fanart_image', self.default_fanart)
358 # determine action route
359 action = actions['default']
360 if category in actions.keys():
361 action = actions[category]
362 # determine if the item should be selected
363 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
364 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
365 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
367 # add recommendations/genres as subfolders (save us some space on the home page)
369 'recommendations': self.get_local_string(30001),
370 'genres': self.get_local_string(30010)
372 for type in i18n_ids.keys():
373 # determine if the lists have contents
374 if len(video_list_ids[type]) > 0:
375 # determine action route
376 action = actions['default']
377 if type in actions.keys():
378 action = actions[type]
379 # determine if the item should be selected
380 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
381 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
382 li_rec.setProperty('fanart_image', self.default_fanart)
383 url_rec = build_url({'action': action, 'type': type})
384 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
386 # add search as subfolder
387 action = actions['default']
388 if 'search' in actions.keys():
389 action = actions[type]
390 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
391 li_rec.setProperty('fanart_image', self.default_fanart)
392 url_rec = build_url({'action': action, 'type': 'search'})
393 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
396 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
397 xbmcplugin.endOfDirectory(self.plugin_handle)
399 # (re)select the previously selected main menu entry
401 for item in preselect_items:
403 preselected_list_item = idx if item else None
404 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
405 if preselected_list_item != None:
406 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
409 def build_video_listing (self, video_list, actions, type, build_url):
410 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
414 video_list_ids : :obj:`dict` of :obj:`str`
417 actions : :obj:`dict` of :obj:`str`
418 Dictionary of actions to build subsequent routes
421 None or 'queue' f.e. when it´s a special video lists
423 build_url : :obj:`fn`
424 Function to build the subsequent routes
431 for video_list_id in video_list:
432 video = video_list[video_list_id]
433 li = xbmcgui.ListItem(label=video['title'])
434 # add some art to the item
435 li = self._generate_art_info(entry=video, li=li)
436 # it´s a show, so we need a subfolder & route (for seasons)
438 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
439 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
440 if video_list[video_list_id]['type'] == 'movie':
441 # it´s a movie, so we need no subfolder & a route to play it
443 url = build_url({'action': 'play_video', 'video_id': video_list_id})
445 li = self._generate_entry_info(entry=video, li=li)
446 li = self._generate_context_menu_items(entry=video, li=li)
447 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
449 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
450 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
451 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
452 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
453 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
454 xbmcplugin.endOfDirectory(self.plugin_handle)
457 def build_search_result_listing (self, video_list, actions, build_url):
458 """Builds the search results list Kodi screen
462 video_list : :obj:`dict` of :obj:`str`
463 List of videos or shows
465 actions : :obj:`dict` of :obj:`str`
466 Dictionary of actions to build subsequent routes
468 build_url : :obj:`fn`
469 Function to build the subsequent routes
476 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
478 def build_no_seasons_available (self):
479 """Builds the season list screen if no seasons could be found
486 self.show_no_seasons_notification()
487 xbmcplugin.endOfDirectory(self.plugin_handle)
490 def build_no_search_results_available (self, build_url, action):
491 """Builds the search results screen if no matches could be found
496 Action paramter to build the subsequent routes
498 build_url : :obj:`fn`
499 Function to build the subsequent routes
506 self.show_no_search_results_notification()
507 return xbmcplugin.endOfDirectory(self.plugin_handle)
509 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
510 """Builds the video lists screen for user subfolders (genres & recommendations)
514 video_list_ids : :obj:`dict` of :obj:`str`
518 List type (genre or recommendation)
521 Action paramter to build the subsequent routes
523 build_url : :obj:`fn`
524 Function to build the subsequent routes
531 for video_list_id in video_list_ids:
532 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
533 li.setProperty('fanart_image', self.default_fanart)
534 url = build_url({'action': action, 'video_list_id': video_list_id})
535 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
537 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
538 xbmcplugin.endOfDirectory(self.plugin_handle)
541 def build_season_listing (self, seasons_sorted, season_list, build_url):
542 """Builds the season list screen for a show
546 seasons_sorted : :obj:`list` of :obj:`str`
547 Sorted season indexes
549 season_list : :obj:`dict` of :obj:`str`
550 List of season entries
552 build_url : :obj:`fn`
553 Function to build the subsequent routes
560 for index in seasons_sorted:
561 for season_id in season_list:
562 season = season_list[season_id]
563 if int(season['idx']) == index:
564 li = xbmcgui.ListItem(label=season['text'])
565 # add some art to the item
566 li = self._generate_art_info(entry=season, li=li)
568 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
569 li = self._generate_context_menu_items(entry=season, li=li)
570 url = build_url({'action': 'episode_list', 'season_id': season_id})
571 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
573 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
574 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
575 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
576 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
577 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
578 xbmcplugin.endOfDirectory(self.plugin_handle)
581 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
582 """Builds the episode list screen for a season of a show
586 episodes_sorted : :obj:`list` of :obj:`str`
587 Sorted episode indexes
589 episode_list : :obj:`dict` of :obj:`str`
590 List of episode entries
592 build_url : :obj:`fn`
593 Function to build the subsequent routes
600 for index in episodes_sorted:
601 for episode_id in episode_list:
602 episode = episode_list[episode_id]
603 if int(episode['episode']) == index:
604 li = xbmcgui.ListItem(label=episode['title'])
605 # add some art to the item
606 li = self._generate_art_info(entry=episode, li=li)
608 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
609 li = self._generate_context_menu_items(entry=episode, li=li)
610 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
611 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
613 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
614 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
615 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
616 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
617 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
618 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
619 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
620 xbmcplugin.endOfDirectory(self.plugin_handle)
623 def play_item (self, esn, video_id, start_offset=-1):
629 ESN needed for Widevine/Inputstream
631 video_id : :obj:`str`
632 ID of the video that should be played
634 start_offset : :obj:`str`
635 Offset to resume playback from (in seconds)
642 inputstream_addon = self.get_inputstream_addon()
643 if inputstream_addon == None:
644 self.show_missing_inputstream_addon_notification()
645 self.log(msg='Inputstream addon not found')
649 self.track_event('playVideo')
651 # check esn in settings
652 settings_esn = str(self.addon.getSetting('esn'))
653 if len(settings_esn) == 0:
654 self.addon.setSetting('esn', str(esn))
656 # inputstream addon properties
657 msl_service_url = 'http://localhost:' + str(self.addon.getSetting('msl_service_port'))
658 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
659 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
660 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
661 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
662 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=')
663 play_item.setProperty('inputstreamaddon', inputstream_addon)
665 # check if we have a bookmark e.g. start offset position
666 if int(start_offset) > 0:
667 play_item.setProperty('StartOffset', str(start_offset) + '.0')
668 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
670 def _generate_art_info (self, entry, li):
671 """Adds the art info from an entry to a Kodi list item
675 entry : :obj:`dict` of :obj:`str`
676 Entry that should be turned into a list item
678 li : :obj:`XMBC.ListItem`
679 Kodi list item instance
684 Kodi list item instance
686 art = {'fanart': self.default_fanart}
687 if 'boxarts' in dict(entry).keys():
689 'poster': entry['boxarts']['big'],
690 'landscape': entry['boxarts']['big'],
691 'thumb': entry['boxarts']['small'],
692 'fanart': entry['boxarts']['big']
694 if 'interesting_moment' in dict(entry).keys():
696 'poster': entry['interesting_moment'],
697 'fanart': entry['interesting_moment']
699 if 'thumb' in dict(entry).keys():
700 art.update({'thumb': entry['thumb']})
701 if 'fanart' in dict(entry).keys():
702 art.update({'fanart': entry['fanart']})
703 if 'poster' in dict(entry).keys():
704 art.update({'poster': entry['poster']})
708 def _generate_entry_info (self, entry, li, base_info={}):
709 """Adds the item info from an entry to a Kodi list item
713 entry : :obj:`dict` of :obj:`str`
714 Entry that should be turned into a list item
716 li : :obj:`XMBC.ListItem`
717 Kodi list item instance
719 base_info : :obj:`dict` of :obj:`str`
720 Additional info that overrules the entry info
725 Kodi list item instance
728 entry_keys = entry.keys()
729 if 'cast' in entry_keys and len(entry['cast']) > 0:
730 infos.update({'cast': entry['cast']})
731 if 'creators' in entry_keys and len(entry['creators']) > 0:
732 infos.update({'writer': entry['creators'][0]})
733 if 'directors' in entry_keys and len(entry['directors']) > 0:
734 infos.update({'director': entry['directors'][0]})
735 if 'genres' in entry_keys and len(entry['genres']) > 0:
736 infos.update({'genre': entry['genres'][0]})
737 if 'maturity' in entry_keys:
738 if 'mpaa' in entry_keys:
739 infos.update({'mpaa': entry['mpaa']})
741 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
742 if 'rating' in entry_keys:
743 infos.update({'rating': int(entry['rating']) * 2})
744 if 'synopsis' in entry_keys:
745 infos.update({'plot': entry['synopsis']})
746 if 'plot' in entry_keys:
747 infos.update({'plot': entry['plot']})
748 if 'runtime' in entry_keys:
749 infos.update({'duration': entry['runtime']})
750 if 'duration' in entry_keys:
751 infos.update({'duration': entry['duration']})
752 if 'seasons_label' in entry_keys:
753 infos.update({'season': entry['seasons_label']})
754 if 'season' in entry_keys:
755 infos.update({'season': entry['season']})
756 if 'title' in entry_keys:
757 infos.update({'title': entry['title']})
758 if 'type' in entry_keys:
759 if entry['type'] == 'movie' or entry['type'] == 'episode':
760 li.setProperty('IsPlayable', 'true')
761 if 'mediatype' in entry_keys:
762 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
763 li.setProperty('IsPlayable', 'true')
764 infos.update({'mediatype': entry['mediatype']})
765 if 'watched' in entry_keys:
766 infos.update({'playcount': (1, 0)[entry['watched']]})
767 if 'index' in entry_keys:
768 infos.update({'episode': entry['index']})
769 if 'episode' in entry_keys:
770 infos.update({'episode': entry['episode']})
771 if 'year' in entry_keys:
772 infos.update({'year': entry['year']})
773 if 'quality' in entry_keys:
774 quality = {'width': '960', 'height': '540'}
775 if entry['quality'] == '720':
776 quality = {'width': '1280', 'height': '720'}
777 if entry['quality'] == '1080':
778 quality = {'width': '1920', 'height': '1080'}
779 li.addStreamInfo('video', quality)
780 li.setInfo('video', infos)
783 def _generate_context_menu_items (self, entry, li):
784 """Adds context menue items to a Kodi list item
788 entry : :obj:`dict` of :obj:`str`
789 Entry that should be turned into a list item
791 li : :obj:`XMBC.ListItem`
792 Kodi list item instance
796 Kodi list item instance
800 entry_keys = entry.keys()
802 # action item templates
803 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
804 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
806 ['export_to_library', self.get_local_string(30018), 'export'],
807 ['remove_from_library', self.get_local_string(30030), 'remove'],
808 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
809 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
810 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
813 # build concrete action items
814 for action_item in actions:
815 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
817 # add or remove the movie/show/season/episode from & to the users "My List"
818 if 'in_my_list' in entry_keys:
819 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
820 elif 'queue' in entry_keys:
821 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
822 elif 'my_list' in entry_keys:
823 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
824 # rate the movie/show/season/episode on Netflix
825 items.append(action['rate_on_netflix'])
827 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
828 if 'type' in entry_keys:
830 if entry['type'] == 'movie':
831 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
832 items.append(action[action_type])
834 if entry['type'] == 'show' and 'title' in entry_keys:
835 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
836 items.append(action[action_type])
839 li.addContextMenuItems(items)
842 def log (self, msg, level=xbmc.LOGDEBUG):
843 """Adds a log entry to the Kodi log
848 Entry that should be turned into a list item
853 if isinstance(msg, unicode):
854 msg = msg.encode('utf-8')
855 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
857 def get_local_string (self, string_id):
858 """Returns the localized version of a string
862 string_id : :obj:`int`
863 ID of the string that shoudl be fetched
868 Requested string or empty string
870 src = xbmc if string_id < 30000 else self.addon
871 locString = src.getLocalizedString(string_id)
872 if isinstance(locString, unicode):
873 locString = locString.encode('utf-8')
876 def get_inputstream_addon (self):
877 """Checks if the inputstream addon is installed & enabled.
878 Returns the type of the inputstream addon used or None if not found
883 Inputstream addon or None
885 type = 'inputstream.adaptive'
889 'method': 'Addons.GetAddonDetails',
892 'properties': ['enabled']
895 response = xbmc.executeJSONRPC(json.dumps(payload))
896 data = json.loads(response)
897 if not 'error' in data.keys():
898 if data['result']['addon']['enabled'] == True:
902 def set_library (self, library):
903 """Adds an instance of the Library class
907 library : :obj:`Library`
908 instance of the Library class
910 self.library = library
912 def track_event(self, event):
914 Send a tracking event if tracking is enabled
915 :param event: the string idetifier of the event
918 # Check if tracking is enabled
919 enable_tracking = (self.addon.getSetting('enable_tracking') == 'true')
921 #Get or Create Tracking id
922 tracking_id = self.addon.getSetting('tracking_id')
923 if tracking_id is '':
924 tracking_id = str(uuid4())
925 self.addon.setSetting('tracking_id', tracking_id)
926 # Send the tracking event
927 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
928 tracker.send('event', event)