2 # -*- coding: utf-8 -*-
4 # Created on: 13.01.2017
12 from os.path import join, isfile
13 from urllib import urlencode
14 from xbmcaddon import Addon
15 from uuid import uuid4
16 from UniversalAnalytics import Tracker
18 import cPickle as pickle
23 """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
25 def __init__ (self, plugin_handle=None, base_url=None):
26 """Fetches all needed info from Kodi & configures the baseline of the plugin
30 plugin_handle : :obj:`int`
36 addon = self.get_addon()
37 self.plugin_handle = plugin_handle
38 self.base_url = base_url
39 self.plugin = addon.getAddonInfo('name')
40 self.version = addon.getAddonInfo('version')
41 self.base_data_path = xbmc.translatePath(addon.getAddonInfo('profile'))
42 self.home_path = xbmc.translatePath('special://home')
43 self.plugin_path = addon.getAddonInfo('path')
44 self.cookie_path = self.base_data_path + 'COOKIE'
45 self.data_path = self.base_data_path + 'DATA'
46 self.config_path = join(self.base_data_path, 'config')
47 self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
48 self.verb_log = addon.getSetting('logging') == 'true'
49 self.default_fanart = addon.getAddonInfo('fanart')
54 """Returns a fresh addon instance"""
58 """Refresh the current list"""
59 return xbmc.executebuiltin('Container.Refresh')
61 def show_rating_dialog (self):
62 """Asks the user for a movie rating
67 Movie rating between 0 & 10
69 dlg = xbmcgui.Dialog()
70 return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
72 def show_search_term_dialog (self):
73 """Asks the user for a term to query the netflix search for
80 dlg = xbmcgui.Dialog()
81 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
86 def show_add_to_library_title_dialog (self, original_title):
87 """Asks the user for an alternative title for the show/movie that gets exported to the local library
91 original_title : :obj:`str`
92 Original title of the show (as suggested by the addon)
99 dlg = xbmcgui.Dialog()
100 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
102 def show_password_dialog (self):
103 """Asks the user for its Netflix password
110 dlg = xbmcgui.Dialog()
111 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
113 def show_email_dialog (self):
114 """Asks the user for its Netflix account email
119 Netflix account email
121 dlg = xbmcgui.Dialog()
122 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
124 def show_login_failed_notification (self):
125 """Shows notification that the login failed
132 dialog = xbmcgui.Dialog()
133 dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
136 def show_missing_inputstream_addon_notification (self):
137 """Shows notification that the inputstream addon couldn't be found
144 dialog = xbmcgui.Dialog()
145 dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
148 def show_no_search_results_notification (self):
149 """Shows notification that no search results could be found
156 dialog = xbmcgui.Dialog()
157 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
160 def show_no_seasons_notification (self):
161 """Shows notification that no seasons be found
168 dialog = xbmcgui.Dialog()
169 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
172 def set_setting (self, key, value):
173 """Public interface for the addons setSetting method
178 Setting could be set or not
180 return self.get_addon().setSetting(key, value)
182 def get_credentials (self):
183 """Returns the users stored credentials
187 :obj:`dict` of :obj:`str`
188 The users stored account data
191 'email': self.get_addon().getSetting('email'),
192 'password': self.get_addon().getSetting('password')
197 Returns the esn from settings
199 self.log(msg='Is FILE: ' + str(isfile(self.msl_data_path + 'msl_data.json')))
200 self.log(msg=self.get_addon().getSetting('esn'))
201 return self.get_addon().getSetting('esn')
203 def set_esn(self, esn):
205 Returns the esn from settings
207 stored_esn = self.get_esn()
208 if not stored_esn and esn:
209 self.set_setting('esn', esn)
210 self.delete_manifest_data()
214 def delete_manifest_data(self):
215 if isfile(self.msl_data_path + 'msl_data.json'):
216 remove(self.msl_data_path + 'msl_data.json')
217 if isfile(self.msl_data_path + 'manifest.json'):
218 remove(self.msl_data_path + 'manifest.json')
219 msl = MSL(kodi_helper=self)
220 msl.perform_key_handshake()
223 def get_dolby_setting(self):
225 Returns if the dolby sound is enabled
228 return self.get_addon().getSetting('enable_dolby_sound') == 'true'
230 def get_custom_library_settings (self):
231 """Returns the settings in regards to the custom library folder(s)
235 :obj:`dict` of :obj:`str`
236 The users library settings
239 'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
240 'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
243 def get_ssl_verification_setting (self):
244 """Returns the setting that describes if we should verify the ssl transport when loading data
251 return self.get_addon().getSetting('ssl_verification') == 'true'
253 def set_main_menu_selection (self, type):
254 """Persist the chosen main menu entry in memory
261 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
263 def get_main_menu_selection (self):
264 """Gets the persisted chosen main menu entry from memory
269 The last chosen main menu entry
271 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
273 def setup_memcache (self):
274 """Sets up the memory cache if not existant"""
275 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
276 # no cache setup yet, create one
277 if len(cached_items) < 1:
278 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
280 def invalidate_memcache (self):
281 """Invalidates the memory cache"""
282 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
284 def has_cached_item (self, cache_id):
285 """Checks if the requested item is in memory cache
289 cache_id : :obj:`str`
290 ID of the cache entry
297 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
298 return cache_id in cached_items.keys()
300 def get_cached_item (self, cache_id):
301 """Returns an item from the in memory cache
305 cache_id : :obj:`str`
306 ID of the cache entry
311 Contents of the requested cache item or none
313 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
314 if self.has_cached_item(cache_id) != True:
316 return cached_items[cache_id]
318 def add_cached_item (self, cache_id, contents):
319 """Adds an item to the in memory cache
323 cache_id : :obj:`str`
324 ID of the cache entry
329 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
330 cached_items.update({cache_id: contents})
331 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
333 def build_profiles_listing (self, profiles, action, build_url):
334 """Builds the profiles list Kodi screen
338 profiles : :obj:`dict` of :obj:`str`
339 List of user profiles
342 Action paramter to build the subsequent routes
344 build_url : :obj:`fn`
345 Function to build the subsequent routes
352 for profile_id in profiles:
353 profile = profiles[profile_id]
354 url = build_url({'action': action, 'profile_id': profile_id})
355 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
356 li.setProperty('fanart_image', self.default_fanart)
357 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
358 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
359 xbmcplugin.endOfDirectory(self.plugin_handle)
362 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
363 """Builds the video lists (my list, continue watching, etc.) Kodi screen
367 video_list_ids : :obj:`dict` of :obj:`str`
370 user_list_order : :obj:`list` of :obj:`str`
371 Ordered user lists, to determine what should be displayed in the main menue
373 actions : :obj:`dict` of :obj:`str`
374 Dictionary of actions to build subsequent routes
376 build_url : :obj:`fn`
377 Function to build the subsequent routes
385 for category in user_list_order:
386 for video_list_id in video_list_ids['user']:
387 if video_list_ids['user'][video_list_id]['name'] == category:
388 label = video_list_ids['user'][video_list_id]['displayName']
389 if category == 'netflixOriginals':
390 label = label.capitalize()
391 li = xbmcgui.ListItem(label=label)
392 li.setProperty('fanart_image', self.default_fanart)
393 # determine action route
394 action = actions['default']
395 if category in actions.keys():
396 action = actions[category]
397 # determine if the item should be selected
398 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
399 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
400 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
402 # add recommendations/genres as subfolders (save us some space on the home page)
404 'recommendations': self.get_local_string(30001),
405 'genres': self.get_local_string(30010)
407 for type in i18n_ids.keys():
408 # determine if the lists have contents
409 if len(video_list_ids[type]) > 0:
410 # determine action route
411 action = actions['default']
412 if type in actions.keys():
413 action = actions[type]
414 # determine if the item should be selected
415 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
416 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
417 li_rec.setProperty('fanart_image', self.default_fanart)
418 url_rec = build_url({'action': action, 'type': type})
419 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
421 # add search as subfolder
422 action = actions['default']
423 if 'search' in actions.keys():
424 action = actions[type]
425 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
426 li_rec.setProperty('fanart_image', self.default_fanart)
427 url_rec = build_url({'action': action, 'type': 'search'})
428 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
431 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
432 xbmcplugin.endOfDirectory(self.plugin_handle)
434 # (re)select the previously selected main menu entry
436 for item in preselect_items:
438 preselected_list_item = idx if item else None
439 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
440 if preselected_list_item != None:
441 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
444 def build_video_listing (self, video_list, actions, type, build_url):
445 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
449 video_list_ids : :obj:`dict` of :obj:`str`
452 actions : :obj:`dict` of :obj:`str`
453 Dictionary of actions to build subsequent routes
456 None or 'queue' f.e. when it´s a special video lists
458 build_url : :obj:`fn`
459 Function to build the subsequent routes
466 for video_list_id in video_list:
467 video = video_list[video_list_id]
468 li = xbmcgui.ListItem(label=video['title'])
469 # add some art to the item
470 li = self._generate_art_info(entry=video, li=li)
471 # it´s a show, so we need a subfolder & route (for seasons)
473 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
474 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
475 if video_list[video_list_id]['type'] == 'movie':
476 # it´s a movie, so we need no subfolder & a route to play it
478 url = build_url({'action': 'play_video', 'video_id': video_list_id})
480 li = self._generate_entry_info(entry=video, li=li)
481 li = self._generate_context_menu_items(entry=video, li=li)
482 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
484 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
485 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
486 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
487 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
488 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
489 xbmcplugin.endOfDirectory(self.plugin_handle)
492 def build_search_result_listing (self, video_list, actions, build_url):
493 """Builds the search results list Kodi screen
497 video_list : :obj:`dict` of :obj:`str`
498 List of videos or shows
500 actions : :obj:`dict` of :obj:`str`
501 Dictionary of actions to build subsequent routes
503 build_url : :obj:`fn`
504 Function to build the subsequent routes
511 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
513 def build_no_seasons_available (self):
514 """Builds the season list screen if no seasons could be found
521 self.show_no_seasons_notification()
522 xbmcplugin.endOfDirectory(self.plugin_handle)
525 def build_no_search_results_available (self, build_url, action):
526 """Builds the search results screen if no matches could be found
531 Action paramter to build the subsequent routes
533 build_url : :obj:`fn`
534 Function to build the subsequent routes
541 self.show_no_search_results_notification()
542 return xbmcplugin.endOfDirectory(self.plugin_handle)
544 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
545 """Builds the video lists screen for user subfolders (genres & recommendations)
549 video_list_ids : :obj:`dict` of :obj:`str`
553 List type (genre or recommendation)
556 Action paramter to build the subsequent routes
558 build_url : :obj:`fn`
559 Function to build the subsequent routes
566 for video_list_id in video_list_ids:
567 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
568 li.setProperty('fanart_image', self.default_fanart)
569 url = build_url({'action': action, 'video_list_id': video_list_id})
570 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
572 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
573 xbmcplugin.endOfDirectory(self.plugin_handle)
576 def build_season_listing (self, seasons_sorted, season_list, build_url):
577 """Builds the season list screen for a show
581 seasons_sorted : :obj:`list` of :obj:`str`
582 Sorted season indexes
584 season_list : :obj:`dict` of :obj:`str`
585 List of season entries
587 build_url : :obj:`fn`
588 Function to build the subsequent routes
595 for index in seasons_sorted:
596 for season_id in season_list:
597 season = season_list[season_id]
598 if int(season['idx']) == index:
599 li = xbmcgui.ListItem(label=season['text'])
600 # add some art to the item
601 li = self._generate_art_info(entry=season, li=li)
603 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
604 li = self._generate_context_menu_items(entry=season, li=li)
605 url = build_url({'action': 'episode_list', 'season_id': season_id})
606 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
608 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
609 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
610 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
611 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
612 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
613 xbmcplugin.endOfDirectory(self.plugin_handle)
616 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
617 """Builds the episode list screen for a season of a show
621 episodes_sorted : :obj:`list` of :obj:`str`
622 Sorted episode indexes
624 episode_list : :obj:`dict` of :obj:`str`
625 List of episode entries
627 build_url : :obj:`fn`
628 Function to build the subsequent routes
635 for index in episodes_sorted:
636 for episode_id in episode_list:
637 episode = episode_list[episode_id]
638 if int(episode['episode']) == index:
639 li = xbmcgui.ListItem(label=episode['title'])
640 # add some art to the item
641 li = self._generate_art_info(entry=episode, li=li)
643 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
644 li = self._generate_context_menu_items(entry=episode, li=li)
645 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
646 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
648 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
649 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
650 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
651 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
652 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
653 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
654 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
655 xbmcplugin.endOfDirectory(self.plugin_handle)
658 def play_item (self, esn, video_id, start_offset=-1):
664 ESN needed for Widevine/Inputstream
666 video_id : :obj:`str`
667 ID of the video that should be played
669 start_offset : :obj:`str`
670 Offset to resume playback from (in seconds)
678 addon = self.get_addon()
679 inputstream_addon = self.get_inputstream_addon()
680 if inputstream_addon == None:
681 self.show_missing_inputstream_addon_notification()
682 self.log(msg='Inputstream addon not found')
686 self.track_event('playVideo')
688 # check esn in settings
689 settings_esn = str(addon.getSetting('esn'))
690 if len(settings_esn) == 0:
691 addon.setSetting('esn', str(esn))
693 # inputstream addon properties
694 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
695 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
696 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
697 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
698 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
699 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=')
700 play_item.setProperty('inputstreamaddon', inputstream_addon)
702 # check if we have a bookmark e.g. start offset position
703 if int(start_offset) > 0:
704 play_item.setProperty('StartOffset', str(start_offset) + '.0')
705 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
707 def _generate_art_info (self, entry, li):
708 """Adds the art info from an entry to a Kodi list item
712 entry : :obj:`dict` of :obj:`str`
713 Entry that should be turned into a list item
715 li : :obj:`XMBC.ListItem`
716 Kodi list item instance
721 Kodi list item instance
723 art = {'fanart': self.default_fanart}
724 if 'boxarts' in dict(entry).keys():
726 'poster': entry['boxarts']['big'],
727 'landscape': entry['boxarts']['big'],
728 'thumb': entry['boxarts']['small'],
729 'fanart': entry['boxarts']['big']
731 if 'interesting_moment' in dict(entry).keys():
733 'poster': entry['interesting_moment'],
734 'fanart': entry['interesting_moment']
736 if 'thumb' in dict(entry).keys():
737 art.update({'thumb': entry['thumb']})
738 if 'fanart' in dict(entry).keys():
739 art.update({'fanart': entry['fanart']})
740 if 'poster' in dict(entry).keys():
741 art.update({'poster': entry['poster']})
745 def _generate_entry_info (self, entry, li, base_info={}):
746 """Adds the item info from an entry to a Kodi list item
750 entry : :obj:`dict` of :obj:`str`
751 Entry that should be turned into a list item
753 li : :obj:`XMBC.ListItem`
754 Kodi list item instance
756 base_info : :obj:`dict` of :obj:`str`
757 Additional info that overrules the entry info
762 Kodi list item instance
765 entry_keys = entry.keys()
766 if 'cast' in entry_keys and len(entry['cast']) > 0:
767 infos.update({'cast': entry['cast']})
768 if 'creators' in entry_keys and len(entry['creators']) > 0:
769 infos.update({'writer': entry['creators'][0]})
770 if 'directors' in entry_keys and len(entry['directors']) > 0:
771 infos.update({'director': entry['directors'][0]})
772 if 'genres' in entry_keys and len(entry['genres']) > 0:
773 infos.update({'genre': entry['genres'][0]})
774 if 'maturity' in entry_keys:
775 if 'mpaa' in entry_keys:
776 infos.update({'mpaa': entry['mpaa']})
778 if entry.get('maturity', None) is not None:
779 if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
780 infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
781 if 'rating' in entry_keys:
782 infos.update({'rating': int(entry['rating']) * 2})
783 if 'synopsis' in entry_keys:
784 infos.update({'plot': entry['synopsis']})
785 if 'plot' in entry_keys:
786 infos.update({'plot': entry['plot']})
787 if 'runtime' in entry_keys:
788 infos.update({'duration': entry['runtime']})
789 if 'duration' in entry_keys:
790 infos.update({'duration': entry['duration']})
791 if 'seasons_label' in entry_keys:
792 infos.update({'season': entry['seasons_label']})
793 if 'season' in entry_keys:
794 infos.update({'season': entry['season']})
795 if 'title' in entry_keys:
796 infos.update({'title': entry['title']})
797 if 'type' in entry_keys:
798 if entry['type'] == 'movie' or entry['type'] == 'episode':
799 li.setProperty('IsPlayable', 'true')
800 if 'mediatype' in entry_keys:
801 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
802 li.setProperty('IsPlayable', 'true')
803 infos.update({'mediatype': entry['mediatype']})
804 if 'watched' in entry_keys:
805 infos.update({'playcount': (1, 0)[entry['watched']]})
806 if 'index' in entry_keys:
807 infos.update({'episode': entry['index']})
808 if 'episode' in entry_keys:
809 infos.update({'episode': entry['episode']})
810 if 'year' in entry_keys:
811 infos.update({'year': entry['year']})
812 if 'quality' in entry_keys:
813 quality = {'width': '960', 'height': '540'}
814 if entry['quality'] == '720':
815 quality = {'width': '1280', 'height': '720'}
816 if entry['quality'] == '1080':
817 quality = {'width': '1920', 'height': '1080'}
818 li.addStreamInfo('video', quality)
819 li.setInfo('video', infos)
822 def _generate_context_menu_items (self, entry, li):
823 """Adds context menue items to a Kodi list item
827 entry : :obj:`dict` of :obj:`str`
828 Entry that should be turned into a list item
830 li : :obj:`XMBC.ListItem`
831 Kodi list item instance
835 Kodi list item instance
839 entry_keys = entry.keys()
841 # action item templates
842 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
843 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
845 ['export_to_library', self.get_local_string(30018), 'export'],
846 ['remove_from_library', self.get_local_string(30030), 'remove'],
847 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
848 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
849 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
852 # build concrete action items
853 for action_item in actions:
854 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
856 # add or remove the movie/show/season/episode from & to the users "My List"
857 if 'in_my_list' in entry_keys:
858 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
859 elif 'queue' in entry_keys:
860 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
861 elif 'my_list' in entry_keys:
862 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
863 # rate the movie/show/season/episode on Netflix
864 items.append(action['rate_on_netflix'])
866 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
867 if 'type' in entry_keys:
869 if entry['type'] == 'movie':
870 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
871 items.append(action[action_type])
873 if entry['type'] == 'show' and 'title' in entry_keys:
874 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
875 items.append(action[action_type])
878 li.addContextMenuItems(items)
881 def log (self, msg, level=xbmc.LOGDEBUG):
882 """Adds a log entry to the Kodi log
887 Entry that should be turned into a list item
892 if isinstance(msg, unicode):
893 msg = msg.encode('utf-8')
894 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
896 def get_local_string (self, string_id):
897 """Returns the localized version of a string
901 string_id : :obj:`int`
902 ID of the string that shoudl be fetched
907 Requested string or empty string
909 src = xbmc if string_id < 30000 else self.get_addon()
910 locString = src.getLocalizedString(string_id)
911 if isinstance(locString, unicode):
912 locString = locString.encode('utf-8')
915 def get_inputstream_addon (self):
916 """Checks if the inputstream addon is installed & enabled.
917 Returns the type of the inputstream addon used or None if not found
922 Inputstream addon or None
924 type = 'inputstream.adaptive'
928 'method': 'Addons.GetAddonDetails',
931 'properties': ['enabled']
934 response = xbmc.executeJSONRPC(json.dumps(payload))
935 data = json.loads(response)
936 if not 'error' in data.keys():
937 if data['result']['addon']['enabled'] == True:
941 def set_library (self, library):
942 """Adds an instance of the Library class
946 library : :obj:`Library`
947 instance of the Library class
949 self.library = library
951 def track_event(self, event):
953 Send a tracking event if tracking is enabled
954 :param event: the string idetifier of the event
957 addon = self.get_addon()
958 # Check if tracking is enabled
959 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
961 #Get or Create Tracking id
962 tracking_id = addon.getSetting('tracking_id')
963 if tracking_id is '':
964 tracking_id = str(uuid4())
965 addon.setSetting('tracking_id', tracking_id)
966 # Send the tracking event
967 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
968 tracker.send('event', event)