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')
47 self.win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
52 """Refrsh the current list"""
53 return xbmc.executebuiltin('Container.Refresh')
55 def show_rating_dialog (self):
56 """Asks the user for a movie rating
61 Movie rating between 0 & 10
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)
66 def show_search_term_dialog (self):
67 """Asks the user for a term to query the netflix search for
74 dlg = xbmcgui.Dialog()
75 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
80 def show_add_to_library_title_dialog (self, original_title):
81 """Asks the user for an alternative title for the show/movie that gets exported to the local library
85 original_title : :obj:`str`
86 Original title of the show (as suggested by the addon)
93 dlg = xbmcgui.Dialog()
94 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
96 def show_password_dialog (self):
97 """Asks the user for its Netflix password
104 dlg = xbmcgui.Dialog()
105 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
107 def show_email_dialog (self):
108 """Asks the user for its Netflix account email
113 Netflix account email
115 dlg = xbmcgui.Dialog()
116 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
118 def show_login_failed_notification (self):
119 """Shows notification that the login failed
126 dialog = xbmcgui.Dialog()
127 dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
130 def show_missing_inputstream_addon_notification (self):
131 """Shows notification that the inputstream addon couldn't be found
138 dialog = xbmcgui.Dialog()
139 dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
142 def show_no_search_results_notification (self):
143 """Shows notification that no search results could be found
150 dialog = xbmcgui.Dialog()
151 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
154 def show_no_seasons_notification (self):
155 """Shows notification that no seasons be found
162 dialog = xbmcgui.Dialog()
163 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
166 def set_setting (self, key, value):
167 """Public interface for the addons setSetting method
172 Setting could be set or not
174 return self.addon.setSetting(key, value)
176 def get_credentials (self):
177 """Returns the users stored credentials
181 :obj:`dict` of :obj:`str`
182 The users stored account data
185 'email': self.addon.getSetting('email'),
186 'password': self.addon.getSetting('password')
189 def get_custom_library_settings (self):
190 """Returns the settings in regards to the custom library folder(s)
194 :obj:`dict` of :obj:`str`
195 The users library settings
198 'enablelibraryfolder': self.addon.getSetting('enablelibraryfolder'),
199 'customlibraryfolder': self.addon.getSetting('customlibraryfolder')
202 def get_ssl_verification_setting (self):
203 """Returns the setting that describes if we should verify the ssl transport when loading data
210 return self.addon.getSetting('ssl_verification') == 'true'
212 def set_main_menu_selection (self, type):
213 """Persist the chosen main menu entry in memory
220 self.win.setProperty('main_menu_selection', type)
222 def get_main_menu_selection (self):
223 """Gets the persisted chosen main menu entry from memory
228 The last chosen main menu entry
230 return self.win.getProperty('main_menu_selection')
232 def setup_memcache (self):
233 """Sets up the memory cache if not existant"""
234 cached_items = self.win.getProperty('memcache')
235 # no cache setup yet, create one
236 if len(cached_items) < 1:
237 self.win.setProperty('memcache', pickle.dumps({}))
239 def invalidate_memcache (self):
240 """Invalidates the memory cache"""
241 self.win.setProperty('memcache', pickle.dumps({}))
243 def has_cached_item (self, cache_id):
244 """Checks if the requested item is in memory cache
248 cache_id : :obj:`str`
249 ID of the cache entry
256 cached_items = pickle.loads(self.win.getProperty('memcache'))
257 return cache_id in cached_items.keys()
259 def get_cached_item (self, cache_id):
260 """Returns an item from the in memory cache
264 cache_id : :obj:`str`
265 ID of the cache entry
270 Contents of the requested cache item or none
272 cached_items = pickle.loads(self.win.getProperty('memcache'))
273 if self.has_cached_item(cache_id) != True:
275 return cached_items[cache_id]
277 def add_cached_item (self, cache_id, contents):
278 """Adds an item to the in memory cache
282 cache_id : :obj:`str`
283 ID of the cache entry
288 cached_items = pickle.loads(self.win.getProperty('memcache'))
289 cached_items.update({cache_id: contents})
290 self.win.setProperty('memcache', pickle.dumps(cached_items))
292 def build_profiles_listing (self, profiles, action, build_url):
293 """Builds the profiles list Kodi screen
297 profiles : :obj:`dict` of :obj:`str`
298 List of user profiles
301 Action paramter to build the subsequent routes
303 build_url : :obj:`fn`
304 Function to build the subsequent routes
311 for profile_id in profiles:
312 profile = profiles[profile_id]
313 url = build_url({'action': action, 'profile_id': profile_id})
314 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
315 li.setProperty('fanart_image', self.default_fanart)
316 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
317 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
318 xbmcplugin.endOfDirectory(self.plugin_handle)
321 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
322 """Builds the video lists (my list, continue watching, etc.) Kodi screen
326 video_list_ids : :obj:`dict` of :obj:`str`
329 user_list_order : :obj:`list` of :obj:`str`
330 Ordered user lists, to determine what should be displayed in the main menue
332 actions : :obj:`dict` of :obj:`str`
333 Dictionary of actions to build subsequent routes
335 build_url : :obj:`fn`
336 Function to build the subsequent routes
344 for category in user_list_order:
345 for video_list_id in video_list_ids['user']:
346 if video_list_ids['user'][video_list_id]['name'] == category:
347 label = video_list_ids['user'][video_list_id]['displayName']
348 if category == 'netflixOriginals':
349 label = label.capitalize()
350 li = xbmcgui.ListItem(label=label)
351 li.setProperty('fanart_image', self.default_fanart)
352 # determine action route
353 action = actions['default']
354 if category in actions.keys():
355 action = actions[category]
356 # determine if the item should be selected
357 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
358 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
359 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
361 # add recommendations/genres as subfolders (save us some space on the home page)
363 'recommendations': self.get_local_string(30001),
364 'genres': self.get_local_string(30010)
366 for type in i18n_ids.keys():
367 # determine if the lists have contents
368 if len(video_list_ids[type]) > 0:
369 # determine action route
370 action = actions['default']
371 if type in actions.keys():
372 action = actions[type]
373 # determine if the item should be selected
374 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
375 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
376 li_rec.setProperty('fanart_image', self.default_fanart)
377 url_rec = build_url({'action': action, 'type': type})
378 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
380 # add search as subfolder
381 action = actions['default']
382 if 'search' in actions.keys():
383 action = actions[type]
384 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
385 li_rec.setProperty('fanart_image', self.default_fanart)
386 url_rec = build_url({'action': action, 'type': 'search'})
387 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
390 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
391 xbmcplugin.endOfDirectory(self.plugin_handle)
393 # (re)select the previously selected main menu entry
395 for item in preselect_items:
397 preselected_list_item = idx if item else None
398 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
399 if preselected_list_item != None:
400 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(self.win.getFocusId()), str(preselected_list_item)))
403 def build_video_listing (self, video_list, actions, type, build_url):
404 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
408 video_list_ids : :obj:`dict` of :obj:`str`
411 actions : :obj:`dict` of :obj:`str`
412 Dictionary of actions to build subsequent routes
415 None or 'queue' f.e. when it´s a special video lists
417 build_url : :obj:`fn`
418 Function to build the subsequent routes
425 for video_list_id in video_list:
426 video = video_list[video_list_id]
427 if type != 'queue' or (type == 'queue' and video['in_my_list'] == True):
428 li = xbmcgui.ListItem(label=video['title'])
429 # add some art to the item
430 li = self._generate_art_info(entry=video, li=li)
431 # it´s a show, so we need a subfolder & route (for seasons)
433 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
434 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
435 if video_list[video_list_id]['type'] == 'movie':
436 # it´s a movie, so we need no subfolder & a route to play it
438 url = build_url({'action': 'play_video', 'video_id': video_list_id})
440 li = self._generate_entry_info(entry=video, li=li)
441 li = self._generate_context_menu_items(entry=video, li=li)
442 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
444 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
445 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
446 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
447 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
448 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
449 xbmcplugin.endOfDirectory(self.plugin_handle)
452 def build_search_result_listing (self, video_list, actions, build_url):
453 """Builds the search results list Kodi screen
457 video_list : :obj:`dict` of :obj:`str`
458 List of videos or shows
460 actions : :obj:`dict` of :obj:`str`
461 Dictionary of actions to build subsequent routes
463 build_url : :obj:`fn`
464 Function to build the subsequent routes
471 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
473 def build_no_seasons_available (self):
474 """Builds the season list screen if no seasons could be found
481 self.show_no_seasons_notification()
482 xbmcplugin.endOfDirectory(self.plugin_handle)
485 def build_no_search_results_available (self, build_url, action):
486 """Builds the search results screen if no matches could be found
491 Action paramter to build the subsequent routes
493 build_url : :obj:`fn`
494 Function to build the subsequent routes
501 self.show_no_search_results_notification()
502 return xbmcplugin.endOfDirectory(self.plugin_handle)
504 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
505 """Builds the video lists screen for user subfolders (genres & recommendations)
509 video_list_ids : :obj:`dict` of :obj:`str`
513 List type (genre or recommendation)
516 Action paramter to build the subsequent routes
518 build_url : :obj:`fn`
519 Function to build the subsequent routes
526 for video_list_id in video_list_ids:
527 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
528 li.setProperty('fanart_image', self.default_fanart)
529 url = build_url({'action': action, 'video_list_id': video_list_id})
530 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
532 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
533 xbmcplugin.endOfDirectory(self.plugin_handle)
536 def build_season_listing (self, seasons_sorted, season_list, build_url):
537 """Builds the season list screen for a show
541 seasons_sorted : :obj:`list` of :obj:`str`
542 Sorted season indexes
544 season_list : :obj:`dict` of :obj:`str`
545 List of season entries
547 build_url : :obj:`fn`
548 Function to build the subsequent routes
555 for index in seasons_sorted:
556 for season_id in season_list:
557 season = season_list[season_id]
558 if int(season['idx']) == index:
559 li = xbmcgui.ListItem(label=season['text'])
560 # add some art to the item
561 li = self._generate_art_info(entry=season, li=li)
563 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
564 li = self._generate_context_menu_items(entry=season, li=li)
565 url = build_url({'action': 'episode_list', 'season_id': season_id})
566 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
568 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
569 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
570 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
571 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
572 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
573 xbmcplugin.endOfDirectory(self.plugin_handle)
576 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
577 """Builds the episode list screen for a season of a show
581 episodes_sorted : :obj:`list` of :obj:`str`
582 Sorted episode indexes
584 episode_list : :obj:`dict` of :obj:`str`
585 List of episode entries
587 build_url : :obj:`fn`
588 Function to build the subsequent routes
595 for index in episodes_sorted:
596 for episode_id in episode_list:
597 episode = episode_list[episode_id]
598 if int(episode['episode']) == index:
599 li = xbmcgui.ListItem(label=episode['title'])
600 # add some art to the item
601 li = self._generate_art_info(entry=episode, li=li)
603 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
604 li = self._generate_context_menu_items(entry=episode, li=li)
605 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
606 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
608 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
609 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
610 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
611 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
612 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
613 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
614 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
615 xbmcplugin.endOfDirectory(self.plugin_handle)
618 def play_item (self, esn, video_id, start_offset=-1):
624 ESN needed for Widevine/Inputstream
626 video_id : :obj:`str`
627 ID of the video that should be played
629 start_offset : :obj:`str`
630 Offset to resume playback from (in seconds)
637 inputstream_addon = self.get_inputstream_addon()
638 if inputstream_addon == None:
639 self.show_missing_inputstream_addon_notification()
640 self.log(msg='Inputstream addon not found')
644 self.track_event('playVideo')
646 # inputstream addon properties
647 msl_service_url = 'http://localhost:' + str(self.addon.getSetting('msl_service_port'))
648 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
649 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
650 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
651 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
652 play_item.setProperty(inputstream_addon + '.server_certificate', 'Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eAGuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRITgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95OEtzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vfS6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0nu39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlGGCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqVDXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nAovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ0fFdCmw=')
653 # TODO: Change when Kodi can handle/trnsfer defaults in hidden values in settings
654 #play_item.setProperty(inputstream_addon + '.server_certificate', self.addon.getSetting('msl_service_certificate'))
655 play_item.setProperty('inputstreamaddon', inputstream_addon)
657 # check if we have a bookmark e.g. start offset position
658 if int(start_offset) > 0:
659 play_item.setProperty('StartOffset', str(start_offset) + '.0')
660 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
662 def _generate_art_info (self, entry, li):
663 """Adds the art info from an entry to a Kodi list item
667 entry : :obj:`dict` of :obj:`str`
668 Entry that should be turned into a list item
670 li : :obj:`XMBC.ListItem`
671 Kodi list item instance
676 Kodi list item instance
678 art = {'fanart': self.default_fanart}
679 if 'boxarts' in dict(entry).keys():
681 'poster': entry['boxarts']['big'],
682 'landscape': entry['boxarts']['big'],
683 'thumb': entry['boxarts']['small'],
684 'fanart': entry['boxarts']['big']
686 if 'interesting_moment' in dict(entry).keys():
688 'poster': entry['interesting_moment'],
689 'fanart': entry['interesting_moment']
691 if 'thumb' in dict(entry).keys():
692 art.update({'thumb': entry['thumb']})
693 if 'fanart' in dict(entry).keys():
694 art.update({'fanart': entry['fanart']})
695 if 'poster' in dict(entry).keys():
696 art.update({'poster': entry['poster']})
700 def _generate_entry_info (self, entry, li, base_info={}):
701 """Adds the item info from an entry to a Kodi list item
705 entry : :obj:`dict` of :obj:`str`
706 Entry that should be turned into a list item
708 li : :obj:`XMBC.ListItem`
709 Kodi list item instance
711 base_info : :obj:`dict` of :obj:`str`
712 Additional info that overrules the entry info
717 Kodi list item instance
720 entry_keys = entry.keys()
721 if 'cast' in entry_keys and len(entry['cast']) > 0:
722 infos.update({'cast': entry['cast']})
723 if 'creators' in entry_keys and len(entry['creators']) > 0:
724 infos.update({'writer': entry['creators'][0]})
725 if 'directors' in entry_keys and len(entry['directors']) > 0:
726 infos.update({'director': entry['directors'][0]})
727 if 'genres' in entry_keys and len(entry['genres']) > 0:
728 infos.update({'genre': entry['genres'][0]})
729 if 'maturity' in entry_keys:
730 if 'mpaa' in entry_keys:
731 infos.update({'mpaa': entry['mpaa']})
733 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
734 if 'rating' in entry_keys:
735 infos.update({'rating': int(entry['rating']) * 2})
736 if 'synopsis' in entry_keys:
737 infos.update({'plot': entry['synopsis']})
738 if 'plot' in entry_keys:
739 infos.update({'plot': entry['plot']})
740 if 'runtime' in entry_keys:
741 infos.update({'duration': entry['runtime']})
742 if 'duration' in entry_keys:
743 infos.update({'duration': entry['duration']})
744 if 'seasons_label' in entry_keys:
745 infos.update({'season': entry['seasons_label']})
746 if 'season' in entry_keys:
747 infos.update({'season': entry['season']})
748 if 'title' in entry_keys:
749 infos.update({'title': entry['title']})
750 if 'type' in entry_keys:
751 if entry['type'] == 'movie' or entry['type'] == 'episode':
752 li.setProperty('IsPlayable', 'true')
753 if 'mediatype' in entry_keys:
754 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
755 li.setProperty('IsPlayable', 'true')
756 infos.update({'mediatype': entry['mediatype']})
757 if 'watched' in entry_keys:
758 infos.update({'playcount': (1, 0)[entry['watched']]})
759 if 'index' in entry_keys:
760 infos.update({'episode': entry['index']})
761 if 'episode' in entry_keys:
762 infos.update({'episode': entry['episode']})
763 if 'year' in entry_keys:
764 infos.update({'year': entry['year']})
765 if 'quality' in entry_keys:
766 quality = {'width': '960', 'height': '540'}
767 if entry['quality'] == '720':
768 quality = {'width': '1280', 'height': '720'}
769 if entry['quality'] == '1080':
770 quality = {'width': '1920', 'height': '1080'}
771 li.addStreamInfo('video', quality)
772 li.setInfo('video', infos)
775 def _generate_context_menu_items (self, entry, li):
776 """Adds context menue items to a Kodi list item
780 entry : :obj:`dict` of :obj:`str`
781 Entry that should be turned into a list item
783 li : :obj:`XMBC.ListItem`
784 Kodi list item instance
788 Kodi list item instance
792 entry_keys = entry.keys()
794 # action item templates
795 encoded_title = urllib.urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
796 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
798 ['export_to_library', self.get_local_string(30018), 'export'],
799 ['remove_from_library', self.get_local_string(30030), 'remove'],
800 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
801 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
802 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
805 # build concrete action items
806 for action_item in actions:
807 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
809 # add or remove the movie/show/season/episode from & to the users "My List"
810 if 'in_my_list' in entry_keys:
811 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
812 elif 'queue' in entry_keys:
813 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
814 elif 'my_list' in entry_keys:
815 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
816 # rate the movie/show/season/episode on Netflix
817 items.append(action['rate_on_netflix'])
819 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
820 if 'type' in entry_keys:
822 if entry['type'] == 'movie':
823 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
824 items.append(action[action_type])
826 if entry['type'] == 'show' and 'title' in entry_keys:
827 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
828 items.append(action[action_type])
831 li.addContextMenuItems(items)
834 def log (self, msg, level=xbmc.LOGDEBUG):
835 """Adds a log entry to the Kodi log
840 Entry that should be turned into a list item
845 if isinstance(msg, unicode):
846 msg = msg.encode('utf-8')
847 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
849 def get_local_string (self, string_id):
850 """Returns the localized version of a string
854 string_id : :obj:`int`
855 ID of the string that shoudl be fetched
860 Requested string or empty string
862 src = xbmc if string_id < 30000 else self.addon
863 locString = src.getLocalizedString(string_id)
864 if isinstance(locString, unicode):
865 locString = locString.encode('utf-8')
868 def get_inputstream_addon (self):
869 """Checks if the inputstream addon is installed & enabled.
870 Returns the type of the inputstream addon used or None if not found
875 Inputstream addon or None
877 type = 'inputstream.adaptive'
881 'method': 'Addons.GetAddonDetails',
884 'properties': ['enabled']
887 response = xbmc.executeJSONRPC(json.dumps(payload))
888 data = json.loads(response)
889 if not 'error' in data.keys():
890 if data['result']['addon']['enabled'] == True:
894 def set_library (self, library):
895 """Adds an instance of the Library class
899 library : :obj:`Library`
900 instance of the Library class
902 self.library = library
904 def track_event(self, event):
906 Send a tracking event if tracking is enabled
907 :param event: the string idetifier of the event
910 # Check if tracking is enabled
911 enable_tracking = (self.addon.getSetting('enable_logging') == 'true')
913 #Get or Create Tracking id
914 tracking_id = self.addon.getSetting('tracking_id')
915 if tracking_id is '':
916 tracking_id = str(uuid.uuid4())
917 self.addon.setSetting('tracking_id', tracking_id)
918 # Send the tracking event
919 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
920 tracker.send('event', event)