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 addon = self.get_addon()
35 self.plugin_handle = plugin_handle
36 self.base_url = base_url
37 self.plugin = addon.getAddonInfo('name')
38 self.version = addon.getAddonInfo('version')
39 self.base_data_path = xbmc.translatePath(addon.getAddonInfo('profile'))
40 self.home_path = xbmc.translatePath('special://home')
41 self.plugin_path = addon.getAddonInfo('path')
42 self.cookie_path = self.base_data_path + 'COOKIE'
43 self.data_path = self.base_data_path + 'DATA'
44 self.config_path = join(self.base_data_path, 'config')
45 self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
46 self.verb_log = addon.getSetting('logging') == 'true'
47 self.default_fanart = addon.getAddonInfo('fanart')
52 """Returns a fresh addon instance"""
56 """Refresh the current list"""
57 return xbmc.executebuiltin('Container.Refresh')
59 def show_rating_dialog (self):
60 """Asks the user for a movie rating
65 Movie rating between 0 & 10
67 dlg = xbmcgui.Dialog()
68 return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
70 def show_search_term_dialog (self):
71 """Asks the user for a term to query the netflix search for
78 dlg = xbmcgui.Dialog()
79 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
84 def show_add_to_library_title_dialog (self, original_title):
85 """Asks the user for an alternative title for the show/movie that gets exported to the local library
89 original_title : :obj:`str`
90 Original title of the show (as suggested by the addon)
97 dlg = xbmcgui.Dialog()
98 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
100 def show_password_dialog (self):
101 """Asks the user for its Netflix password
108 dlg = xbmcgui.Dialog()
109 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
111 def show_email_dialog (self):
112 """Asks the user for its Netflix account email
117 Netflix account email
119 dlg = xbmcgui.Dialog()
120 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
122 def show_login_failed_notification (self):
123 """Shows notification that the login failed
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)
134 def show_missing_inputstream_addon_notification (self):
135 """Shows notification that the inputstream addon couldn't be found
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)
146 def show_no_search_results_notification (self):
147 """Shows notification that no search results could be found
154 dialog = xbmcgui.Dialog()
155 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
158 def show_no_seasons_notification (self):
159 """Shows notification that no seasons be found
166 dialog = xbmcgui.Dialog()
167 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
170 def set_setting (self, key, value):
171 """Public interface for the addons setSetting method
176 Setting could be set or not
178 return self.get_addon().setSetting(key, value)
180 def get_credentials (self):
181 """Returns the users stored credentials
185 :obj:`dict` of :obj:`str`
186 The users stored account data
189 'email': self.get_addon().getSetting('email'),
190 'password': self.get_addon().getSetting('password')
193 def get_dolby_setting(self):
195 Returns if the dolby sound is enabled
198 return self.get_addon().getSetting('enable_dolby_sound') == 'true'
200 def get_custom_library_settings (self):
201 """Returns the settings in regards to the custom library folder(s)
205 :obj:`dict` of :obj:`str`
206 The users library settings
209 'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
210 'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
213 def get_ssl_verification_setting (self):
214 """Returns the setting that describes if we should verify the ssl transport when loading data
221 return self.get_addon().getSetting('ssl_verification') == 'true'
223 def set_main_menu_selection (self, type):
224 """Persist the chosen main menu entry in memory
231 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
233 def get_main_menu_selection (self):
234 """Gets the persisted chosen main menu entry from memory
239 The last chosen main menu entry
241 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
243 def setup_memcache (self):
244 """Sets up the memory cache if not existant"""
245 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
246 # no cache setup yet, create one
247 if len(cached_items) < 1:
248 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
250 def invalidate_memcache (self):
251 """Invalidates the memory cache"""
252 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
254 def has_cached_item (self, cache_id):
255 """Checks if the requested item is in memory cache
259 cache_id : :obj:`str`
260 ID of the cache entry
267 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
268 return cache_id in cached_items.keys()
270 def get_cached_item (self, cache_id):
271 """Returns an item from the in memory cache
275 cache_id : :obj:`str`
276 ID of the cache entry
281 Contents of the requested cache item or none
283 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
284 if self.has_cached_item(cache_id) != True:
286 return cached_items[cache_id]
288 def add_cached_item (self, cache_id, contents):
289 """Adds an item to the in memory cache
293 cache_id : :obj:`str`
294 ID of the cache entry
299 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
300 cached_items.update({cache_id: contents})
301 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
303 def build_profiles_listing (self, profiles, action, build_url):
304 """Builds the profiles list Kodi screen
308 profiles : :obj:`dict` of :obj:`str`
309 List of user profiles
312 Action paramter to build the subsequent routes
314 build_url : :obj:`fn`
315 Function to build the subsequent routes
322 for profile_id in profiles:
323 profile = profiles[profile_id]
324 url = build_url({'action': action, 'profile_id': profile_id})
325 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
326 li.setProperty('fanart_image', self.default_fanart)
327 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
328 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
329 xbmcplugin.endOfDirectory(self.plugin_handle)
332 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
333 """Builds the video lists (my list, continue watching, etc.) Kodi screen
337 video_list_ids : :obj:`dict` of :obj:`str`
340 user_list_order : :obj:`list` of :obj:`str`
341 Ordered user lists, to determine what should be displayed in the main menue
343 actions : :obj:`dict` of :obj:`str`
344 Dictionary of actions to build subsequent routes
346 build_url : :obj:`fn`
347 Function to build the subsequent routes
355 for category in user_list_order:
356 for video_list_id in video_list_ids['user']:
357 if video_list_ids['user'][video_list_id]['name'] == category:
358 label = video_list_ids['user'][video_list_id]['displayName']
359 if category == 'netflixOriginals':
360 label = label.capitalize()
361 li = xbmcgui.ListItem(label=label)
362 li.setProperty('fanart_image', self.default_fanart)
363 # determine action route
364 action = actions['default']
365 if category in actions.keys():
366 action = actions[category]
367 # determine if the item should be selected
368 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
369 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
370 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
372 # add recommendations/genres as subfolders (save us some space on the home page)
374 'recommendations': self.get_local_string(30001),
375 'genres': self.get_local_string(30010)
377 for type in i18n_ids.keys():
378 # determine if the lists have contents
379 if len(video_list_ids[type]) > 0:
380 # determine action route
381 action = actions['default']
382 if type in actions.keys():
383 action = actions[type]
384 # determine if the item should be selected
385 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
386 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
387 li_rec.setProperty('fanart_image', self.default_fanart)
388 url_rec = build_url({'action': action, 'type': type})
389 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
391 # add search as subfolder
392 action = actions['default']
393 if 'search' in actions.keys():
394 action = actions[type]
395 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
396 li_rec.setProperty('fanart_image', self.default_fanart)
397 url_rec = build_url({'action': action, 'type': 'search'})
398 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
401 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
402 xbmcplugin.endOfDirectory(self.plugin_handle)
404 # (re)select the previously selected main menu entry
406 for item in preselect_items:
408 preselected_list_item = idx if item else None
409 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
410 if preselected_list_item != None:
411 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
414 def build_video_listing (self, video_list, actions, type, build_url):
415 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
419 video_list_ids : :obj:`dict` of :obj:`str`
422 actions : :obj:`dict` of :obj:`str`
423 Dictionary of actions to build subsequent routes
426 None or 'queue' f.e. when it´s a special video lists
428 build_url : :obj:`fn`
429 Function to build the subsequent routes
436 for video_list_id in video_list:
437 video = video_list[video_list_id]
438 li = xbmcgui.ListItem(label=video['title'])
439 # add some art to the item
440 li = self._generate_art_info(entry=video, li=li)
441 # it´s a show, so we need a subfolder & route (for seasons)
443 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
444 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
445 if video_list[video_list_id]['type'] == 'movie':
446 # it´s a movie, so we need no subfolder & a route to play it
448 url = build_url({'action': 'play_video', 'video_id': video_list_id})
450 li = self._generate_entry_info(entry=video, li=li)
451 li = self._generate_context_menu_items(entry=video, li=li)
452 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
454 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
455 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
456 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
457 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
458 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
459 xbmcplugin.endOfDirectory(self.plugin_handle)
462 def build_search_result_listing (self, video_list, actions, build_url):
463 """Builds the search results list Kodi screen
467 video_list : :obj:`dict` of :obj:`str`
468 List of videos or shows
470 actions : :obj:`dict` of :obj:`str`
471 Dictionary of actions to build subsequent routes
473 build_url : :obj:`fn`
474 Function to build the subsequent routes
481 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
483 def build_no_seasons_available (self):
484 """Builds the season list screen if no seasons could be found
491 self.show_no_seasons_notification()
492 xbmcplugin.endOfDirectory(self.plugin_handle)
495 def build_no_search_results_available (self, build_url, action):
496 """Builds the search results screen if no matches could be found
501 Action paramter to build the subsequent routes
503 build_url : :obj:`fn`
504 Function to build the subsequent routes
511 self.show_no_search_results_notification()
512 return xbmcplugin.endOfDirectory(self.plugin_handle)
514 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
515 """Builds the video lists screen for user subfolders (genres & recommendations)
519 video_list_ids : :obj:`dict` of :obj:`str`
523 List type (genre or recommendation)
526 Action paramter to build the subsequent routes
528 build_url : :obj:`fn`
529 Function to build the subsequent routes
536 for video_list_id in video_list_ids:
537 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
538 li.setProperty('fanart_image', self.default_fanart)
539 url = build_url({'action': action, 'video_list_id': video_list_id})
540 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
542 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
543 xbmcplugin.endOfDirectory(self.plugin_handle)
546 def build_season_listing (self, seasons_sorted, season_list, build_url):
547 """Builds the season list screen for a show
551 seasons_sorted : :obj:`list` of :obj:`str`
552 Sorted season indexes
554 season_list : :obj:`dict` of :obj:`str`
555 List of season entries
557 build_url : :obj:`fn`
558 Function to build the subsequent routes
565 for index in seasons_sorted:
566 for season_id in season_list:
567 season = season_list[season_id]
568 if int(season['idx']) == index:
569 li = xbmcgui.ListItem(label=season['text'])
570 # add some art to the item
571 li = self._generate_art_info(entry=season, li=li)
573 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
574 li = self._generate_context_menu_items(entry=season, li=li)
575 url = build_url({'action': 'episode_list', 'season_id': season_id})
576 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
578 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
579 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
580 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
581 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
582 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
583 xbmcplugin.endOfDirectory(self.plugin_handle)
586 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
587 """Builds the episode list screen for a season of a show
591 episodes_sorted : :obj:`list` of :obj:`str`
592 Sorted episode indexes
594 episode_list : :obj:`dict` of :obj:`str`
595 List of episode entries
597 build_url : :obj:`fn`
598 Function to build the subsequent routes
605 for index in episodes_sorted:
606 for episode_id in episode_list:
607 episode = episode_list[episode_id]
608 if int(episode['episode']) == index:
609 li = xbmcgui.ListItem(label=episode['title'])
610 # add some art to the item
611 li = self._generate_art_info(entry=episode, li=li)
613 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
614 li = self._generate_context_menu_items(entry=episode, li=li)
615 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
616 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
618 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
619 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
620 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
621 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
622 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
623 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
624 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
625 xbmcplugin.endOfDirectory(self.plugin_handle)
628 def play_item (self, esn, video_id, start_offset=-1):
634 ESN needed for Widevine/Inputstream
636 video_id : :obj:`str`
637 ID of the video that should be played
639 start_offset : :obj:`str`
640 Offset to resume playback from (in seconds)
647 addon = self.get_addon()
648 inputstream_addon = self.get_inputstream_addon()
649 if inputstream_addon == None:
650 self.show_missing_inputstream_addon_notification()
651 self.log(msg='Inputstream addon not found')
655 self.track_event('playVideo')
657 # check esn in settings
658 settings_esn = str(addon.getSetting('esn'))
659 if len(settings_esn) == 0:
660 addon.setSetting('esn', str(esn))
662 # inputstream addon properties
663 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
664 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
665 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
666 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
667 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
668 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=')
669 play_item.setProperty('inputstreamaddon', inputstream_addon)
671 # check if we have a bookmark e.g. start offset position
672 if int(start_offset) > 0:
673 play_item.setProperty('StartOffset', str(start_offset) + '.0')
674 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
676 def _generate_art_info (self, entry, li):
677 """Adds the art info from an entry to a Kodi list item
681 entry : :obj:`dict` of :obj:`str`
682 Entry that should be turned into a list item
684 li : :obj:`XMBC.ListItem`
685 Kodi list item instance
690 Kodi list item instance
692 art = {'fanart': self.default_fanart}
693 if 'boxarts' in dict(entry).keys():
695 'poster': entry['boxarts']['big'],
696 'landscape': entry['boxarts']['big'],
697 'thumb': entry['boxarts']['small'],
698 'fanart': entry['boxarts']['big']
700 if 'interesting_moment' in dict(entry).keys():
702 'poster': entry['interesting_moment'],
703 'fanart': entry['interesting_moment']
705 if 'thumb' in dict(entry).keys():
706 art.update({'thumb': entry['thumb']})
707 if 'fanart' in dict(entry).keys():
708 art.update({'fanart': entry['fanart']})
709 if 'poster' in dict(entry).keys():
710 art.update({'poster': entry['poster']})
714 def _generate_entry_info (self, entry, li, base_info={}):
715 """Adds the item info from an entry to a Kodi list item
719 entry : :obj:`dict` of :obj:`str`
720 Entry that should be turned into a list item
722 li : :obj:`XMBC.ListItem`
723 Kodi list item instance
725 base_info : :obj:`dict` of :obj:`str`
726 Additional info that overrules the entry info
731 Kodi list item instance
734 entry_keys = entry.keys()
735 if 'cast' in entry_keys and len(entry['cast']) > 0:
736 infos.update({'cast': entry['cast']})
737 if 'creators' in entry_keys and len(entry['creators']) > 0:
738 infos.update({'writer': entry['creators'][0]})
739 if 'directors' in entry_keys and len(entry['directors']) > 0:
740 infos.update({'director': entry['directors'][0]})
741 if 'genres' in entry_keys and len(entry['genres']) > 0:
742 infos.update({'genre': entry['genres'][0]})
743 if 'maturity' in entry_keys:
744 if 'mpaa' in entry_keys:
745 infos.update({'mpaa': entry['mpaa']})
747 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
748 if 'rating' in entry_keys:
749 infos.update({'rating': int(entry['rating']) * 2})
750 if 'synopsis' in entry_keys:
751 infos.update({'plot': entry['synopsis']})
752 if 'plot' in entry_keys:
753 infos.update({'plot': entry['plot']})
754 if 'runtime' in entry_keys:
755 infos.update({'duration': entry['runtime']})
756 if 'duration' in entry_keys:
757 infos.update({'duration': entry['duration']})
758 if 'seasons_label' in entry_keys:
759 infos.update({'season': entry['seasons_label']})
760 if 'season' in entry_keys:
761 infos.update({'season': entry['season']})
762 if 'title' in entry_keys:
763 infos.update({'title': entry['title']})
764 if 'type' in entry_keys:
765 if entry['type'] == 'movie' or entry['type'] == 'episode':
766 li.setProperty('IsPlayable', 'true')
767 if 'mediatype' in entry_keys:
768 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
769 li.setProperty('IsPlayable', 'true')
770 infos.update({'mediatype': entry['mediatype']})
771 if 'watched' in entry_keys:
772 infos.update({'playcount': (1, 0)[entry['watched']]})
773 if 'index' in entry_keys:
774 infos.update({'episode': entry['index']})
775 if 'episode' in entry_keys:
776 infos.update({'episode': entry['episode']})
777 if 'year' in entry_keys:
778 infos.update({'year': entry['year']})
779 if 'quality' in entry_keys:
780 quality = {'width': '960', 'height': '540'}
781 if entry['quality'] == '720':
782 quality = {'width': '1280', 'height': '720'}
783 if entry['quality'] == '1080':
784 quality = {'width': '1920', 'height': '1080'}
785 li.addStreamInfo('video', quality)
786 li.setInfo('video', infos)
789 def _generate_context_menu_items (self, entry, li):
790 """Adds context menue items to a Kodi list item
794 entry : :obj:`dict` of :obj:`str`
795 Entry that should be turned into a list item
797 li : :obj:`XMBC.ListItem`
798 Kodi list item instance
802 Kodi list item instance
806 entry_keys = entry.keys()
808 # action item templates
809 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
810 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
812 ['export_to_library', self.get_local_string(30018), 'export'],
813 ['remove_from_library', self.get_local_string(30030), 'remove'],
814 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
815 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
816 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
819 # build concrete action items
820 for action_item in actions:
821 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
823 # add or remove the movie/show/season/episode from & to the users "My List"
824 if 'in_my_list' in entry_keys:
825 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
826 elif 'queue' in entry_keys:
827 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
828 elif 'my_list' in entry_keys:
829 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
830 # rate the movie/show/season/episode on Netflix
831 items.append(action['rate_on_netflix'])
833 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
834 if 'type' in entry_keys:
836 if entry['type'] == 'movie':
837 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
838 items.append(action[action_type])
840 if entry['type'] == 'show' and 'title' in entry_keys:
841 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
842 items.append(action[action_type])
845 li.addContextMenuItems(items)
848 def log (self, msg, level=xbmc.LOGDEBUG):
849 """Adds a log entry to the Kodi log
854 Entry that should be turned into a list item
859 if isinstance(msg, unicode):
860 msg = msg.encode('utf-8')
861 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
863 def get_local_string (self, string_id):
864 """Returns the localized version of a string
868 string_id : :obj:`int`
869 ID of the string that shoudl be fetched
874 Requested string or empty string
876 src = xbmc if string_id < 30000 else self.get_addon()
877 locString = src.getLocalizedString(string_id)
878 if isinstance(locString, unicode):
879 locString = locString.encode('utf-8')
882 def get_inputstream_addon (self):
883 """Checks if the inputstream addon is installed & enabled.
884 Returns the type of the inputstream addon used or None if not found
889 Inputstream addon or None
891 type = 'inputstream.adaptive'
895 'method': 'Addons.GetAddonDetails',
898 'properties': ['enabled']
901 response = xbmc.executeJSONRPC(json.dumps(payload))
902 data = json.loads(response)
903 if not 'error' in data.keys():
904 if data['result']['addon']['enabled'] == True:
908 def set_library (self, library):
909 """Adds an instance of the Library class
913 library : :obj:`Library`
914 instance of the Library class
916 self.library = library
918 def track_event(self, event):
920 Send a tracking event if tracking is enabled
921 :param event: the string idetifier of the event
924 addon = self.get_addon()
925 # Check if tracking is enabled
926 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
928 #Get or Create Tracking id
929 tracking_id = addon.getSetting('tracking_id')
930 if tracking_id is '':
931 tracking_id = str(uuid4())
932 addon.setSetting('tracking_id', tracking_id)
933 # Send the tracking event
934 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
935 tracker.send('event', event)