2 # -*- coding: utf-8 -*-
4 # Created on: 13.01.2017
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, base_url):
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
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')
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 if type != 'queue' or (type == 'queue' and video['in_my_list'] == True):
434 li = xbmcgui.ListItem(label=video['title'])
435 # add some art to the item
436 li = self._generate_art_info(entry=video, li=li)
437 # it´s a show, so we need a subfolder & route (for seasons)
439 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
440 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
441 if video_list[video_list_id]['type'] == 'movie':
442 # it´s a movie, so we need no subfolder & a route to play it
444 url = build_url({'action': 'play_video', 'video_id': video_list_id})
446 li = self._generate_entry_info(entry=video, li=li)
447 li = self._generate_context_menu_items(entry=video, li=li)
448 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
450 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
451 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
452 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
453 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
454 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
455 xbmcplugin.endOfDirectory(self.plugin_handle)
458 def build_search_result_listing (self, video_list, actions, build_url):
459 """Builds the search results list Kodi screen
463 video_list : :obj:`dict` of :obj:`str`
464 List of videos or shows
466 actions : :obj:`dict` of :obj:`str`
467 Dictionary of actions to build subsequent routes
469 build_url : :obj:`fn`
470 Function to build the subsequent routes
477 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
479 def build_no_seasons_available (self):
480 """Builds the season list screen if no seasons could be found
487 self.show_no_seasons_notification()
488 xbmcplugin.endOfDirectory(self.plugin_handle)
491 def build_no_search_results_available (self, build_url, action):
492 """Builds the search results screen if no matches could be found
497 Action paramter to build the subsequent routes
499 build_url : :obj:`fn`
500 Function to build the subsequent routes
507 self.show_no_search_results_notification()
508 return xbmcplugin.endOfDirectory(self.plugin_handle)
510 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
511 """Builds the video lists screen for user subfolders (genres & recommendations)
515 video_list_ids : :obj:`dict` of :obj:`str`
519 List type (genre or recommendation)
522 Action paramter to build the subsequent routes
524 build_url : :obj:`fn`
525 Function to build the subsequent routes
532 for video_list_id in video_list_ids:
533 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
534 li.setProperty('fanart_image', self.default_fanart)
535 url = build_url({'action': action, 'video_list_id': video_list_id})
536 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
538 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
539 xbmcplugin.endOfDirectory(self.plugin_handle)
542 def build_season_listing (self, seasons_sorted, season_list, build_url):
543 """Builds the season list screen for a show
547 seasons_sorted : :obj:`list` of :obj:`str`
548 Sorted season indexes
550 season_list : :obj:`dict` of :obj:`str`
551 List of season entries
553 build_url : :obj:`fn`
554 Function to build the subsequent routes
561 for index in seasons_sorted:
562 for season_id in season_list:
563 season = season_list[season_id]
564 if int(season['idx']) == index:
565 li = xbmcgui.ListItem(label=season['text'])
566 # add some art to the item
567 li = self._generate_art_info(entry=season, li=li)
569 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
570 li = self._generate_context_menu_items(entry=season, li=li)
571 url = build_url({'action': 'episode_list', 'season_id': season_id})
572 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
574 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
575 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
576 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
577 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
578 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
579 xbmcplugin.endOfDirectory(self.plugin_handle)
582 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
583 """Builds the episode list screen for a season of a show
587 episodes_sorted : :obj:`list` of :obj:`str`
588 Sorted episode indexes
590 episode_list : :obj:`dict` of :obj:`str`
591 List of episode entries
593 build_url : :obj:`fn`
594 Function to build the subsequent routes
601 for index in episodes_sorted:
602 for episode_id in episode_list:
603 episode = episode_list[episode_id]
604 if int(episode['episode']) == index:
605 li = xbmcgui.ListItem(label=episode['title'])
606 # add some art to the item
607 li = self._generate_art_info(entry=episode, li=li)
609 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
610 li = self._generate_context_menu_items(entry=episode, li=li)
611 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
612 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
614 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
615 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
616 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
617 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
618 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
619 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
620 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
621 xbmcplugin.endOfDirectory(self.plugin_handle)
624 def play_item (self, esn, video_id, start_offset=-1):
630 ESN needed for Widevine/Inputstream
632 video_id : :obj:`str`
633 ID of the video that should be played
635 start_offset : :obj:`str`
636 Offset to resume playback from (in seconds)
643 inputstream_addon = self.get_inputstream_addon()
644 if inputstream_addon == None:
645 self.show_missing_inputstream_addon_notification()
646 self.log(msg='Inputstream addon not found')
650 self.track_event('playVideo')
652 # inputstream addon properties
653 msl_service_url = 'http://localhost:' + str(self.addon.getSetting('msl_service_port'))
654 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
655 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
656 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
657 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
658 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=')
659 # TODO: Change when Kodi can handle/trnsfer defaults in hidden values in settings
660 #play_item.setProperty(inputstream_addon + '.server_certificate', self.addon.getSetting('msl_service_certificate'))
661 play_item.setProperty('inputstreamaddon', inputstream_addon)
663 # check if we have a bookmark e.g. start offset position
664 if int(start_offset) > 0:
665 play_item.setProperty('StartOffset', str(start_offset) + '.0')
666 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
668 def _generate_art_info (self, entry, li):
669 """Adds the art info from an entry to a Kodi list item
673 entry : :obj:`dict` of :obj:`str`
674 Entry that should be turned into a list item
676 li : :obj:`XMBC.ListItem`
677 Kodi list item instance
682 Kodi list item instance
684 art = {'fanart': self.default_fanart}
685 if 'boxarts' in dict(entry).keys():
687 'poster': entry['boxarts']['big'],
688 'landscape': entry['boxarts']['big'],
689 'thumb': entry['boxarts']['small'],
690 'fanart': entry['boxarts']['big']
692 if 'interesting_moment' in dict(entry).keys():
694 'poster': entry['interesting_moment'],
695 'fanart': entry['interesting_moment']
697 if 'thumb' in dict(entry).keys():
698 art.update({'thumb': entry['thumb']})
699 if 'fanart' in dict(entry).keys():
700 art.update({'fanart': entry['fanart']})
701 if 'poster' in dict(entry).keys():
702 art.update({'poster': entry['poster']})
706 def _generate_entry_info (self, entry, li, base_info={}):
707 """Adds the item info from an entry to a Kodi list item
711 entry : :obj:`dict` of :obj:`str`
712 Entry that should be turned into a list item
714 li : :obj:`XMBC.ListItem`
715 Kodi list item instance
717 base_info : :obj:`dict` of :obj:`str`
718 Additional info that overrules the entry info
723 Kodi list item instance
726 entry_keys = entry.keys()
727 if 'cast' in entry_keys and len(entry['cast']) > 0:
728 infos.update({'cast': entry['cast']})
729 if 'creators' in entry_keys and len(entry['creators']) > 0:
730 infos.update({'writer': entry['creators'][0]})
731 if 'directors' in entry_keys and len(entry['directors']) > 0:
732 infos.update({'director': entry['directors'][0]})
733 if 'genres' in entry_keys and len(entry['genres']) > 0:
734 infos.update({'genre': entry['genres'][0]})
735 if 'maturity' in entry_keys:
736 if 'mpaa' in entry_keys:
737 infos.update({'mpaa': entry['mpaa']})
739 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
740 if 'rating' in entry_keys:
741 infos.update({'rating': int(entry['rating']) * 2})
742 if 'synopsis' in entry_keys:
743 infos.update({'plot': entry['synopsis']})
744 if 'plot' in entry_keys:
745 infos.update({'plot': entry['plot']})
746 if 'runtime' in entry_keys:
747 infos.update({'duration': entry['runtime']})
748 if 'duration' in entry_keys:
749 infos.update({'duration': entry['duration']})
750 if 'seasons_label' in entry_keys:
751 infos.update({'season': entry['seasons_label']})
752 if 'season' in entry_keys:
753 infos.update({'season': entry['season']})
754 if 'title' in entry_keys:
755 infos.update({'title': entry['title']})
756 if 'type' in entry_keys:
757 if entry['type'] == 'movie' or entry['type'] == 'episode':
758 li.setProperty('IsPlayable', 'true')
759 if 'mediatype' in entry_keys:
760 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
761 li.setProperty('IsPlayable', 'true')
762 infos.update({'mediatype': entry['mediatype']})
763 if 'watched' in entry_keys:
764 infos.update({'playcount': (1, 0)[entry['watched']]})
765 if 'index' in entry_keys:
766 infos.update({'episode': entry['index']})
767 if 'episode' in entry_keys:
768 infos.update({'episode': entry['episode']})
769 if 'year' in entry_keys:
770 infos.update({'year': entry['year']})
771 if 'quality' in entry_keys:
772 quality = {'width': '960', 'height': '540'}
773 if entry['quality'] == '720':
774 quality = {'width': '1280', 'height': '720'}
775 if entry['quality'] == '1080':
776 quality = {'width': '1920', 'height': '1080'}
777 li.addStreamInfo('video', quality)
778 li.setInfo('video', infos)
781 def _generate_context_menu_items (self, entry, li):
782 """Adds context menue items to a Kodi list item
786 entry : :obj:`dict` of :obj:`str`
787 Entry that should be turned into a list item
789 li : :obj:`XMBC.ListItem`
790 Kodi list item instance
794 Kodi list item instance
798 entry_keys = entry.keys()
800 # action item templates
801 encoded_title = urllib.urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
802 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
804 ['export_to_library', self.get_local_string(30018), 'export'],
805 ['remove_from_library', self.get_local_string(30030), 'remove'],
806 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
807 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
808 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
811 # build concrete action items
812 for action_item in actions:
813 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
815 # add or remove the movie/show/season/episode from & to the users "My List"
816 if 'in_my_list' in entry_keys:
817 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
818 elif 'queue' in entry_keys:
819 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
820 elif 'my_list' in entry_keys:
821 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
822 # rate the movie/show/season/episode on Netflix
823 items.append(action['rate_on_netflix'])
825 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
826 if 'type' in entry_keys:
828 if entry['type'] == 'movie':
829 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
830 items.append(action[action_type])
832 if entry['type'] == 'show' and 'title' in entry_keys:
833 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
834 items.append(action[action_type])
837 li.addContextMenuItems(items)
840 def log (self, msg, level=xbmc.LOGDEBUG):
841 """Adds a log entry to the Kodi log
846 Entry that should be turned into a list item
851 if isinstance(msg, unicode):
852 msg = msg.encode('utf-8')
853 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
855 def get_local_string (self, string_id):
856 """Returns the localized version of a string
860 string_id : :obj:`int`
861 ID of the string that shoudl be fetched
866 Requested string or empty string
868 src = xbmc if string_id < 30000 else self.addon
869 locString = src.getLocalizedString(string_id)
870 if isinstance(locString, unicode):
871 locString = locString.encode('utf-8')
874 def get_inputstream_addon (self):
875 """Checks if the inputstream addon is installed & enabled.
876 Returns the type of the inputstream addon used or None if not found
881 Inputstream addon or None
883 type = 'inputstream.adaptive'
887 'method': 'Addons.GetAddonDetails',
890 'properties': ['enabled']
893 response = xbmc.executeJSONRPC(json.dumps(payload))
894 data = json.loads(response)
895 if not 'error' in data.keys():
896 if data['result']['addon']['enabled'] == True:
900 def set_library (self, library):
901 """Adds an instance of the Library class
905 library : :obj:`Library`
906 instance of the Library class
908 self.library = library
910 def track_event(self, event):
912 Send a tracking event if tracking is enabled
913 :param event: the string idetifier of the event
916 # Check if tracking is enabled
917 enable_tracking = (self.addon.getSetting('enable_tracking') == 'true')
919 #Get or Create Tracking id
920 tracking_id = self.addon.getSetting('tracking_id')
921 if tracking_id is '':
922 tracking_id = str(uuid.uuid4())
923 self.addon.setSetting('tracking_id', tracking_id)
924 # Send the tracking event
925 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
926 tracker.send('event', event)