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 utils import get_user_agent_for_current_platform
17 from UniversalAnalytics import Tracker
19 import cPickle as pickle
24 """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
26 def __init__ (self, plugin_handle=None, base_url=None):
27 """Fetches all needed info from Kodi & configures the baseline of the plugin
31 plugin_handle : :obj:`int`
37 addon = self.get_addon()
38 self.plugin_handle = plugin_handle
39 self.base_url = base_url
40 self.plugin = addon.getAddonInfo('name')
41 self.version = addon.getAddonInfo('version')
42 self.base_data_path = xbmc.translatePath(addon.getAddonInfo('profile'))
43 self.home_path = xbmc.translatePath('special://home')
44 self.plugin_path = addon.getAddonInfo('path')
45 self.cookie_path = self.base_data_path + 'COOKIE'
46 self.data_path = self.base_data_path + 'DATA'
47 self.config_path = join(self.base_data_path, 'config')
48 self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
49 self.verb_log = addon.getSetting('logging') == 'true'
50 self.default_fanart = addon.getAddonInfo('fanart')
55 """Returns a fresh addon instance"""
59 """Refresh the current list"""
60 return xbmc.executebuiltin('Container.Refresh')
62 def show_rating_dialog (self):
63 """Asks the user for a movie rating
68 Movie rating between 0 & 10
70 dlg = xbmcgui.Dialog()
71 return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
73 def show_search_term_dialog (self):
74 """Asks the user for a term to query the netflix search for
81 dlg = xbmcgui.Dialog()
82 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
87 def show_add_to_library_title_dialog (self, original_title):
88 """Asks the user for an alternative title for the show/movie that gets exported to the local library
92 original_title : :obj:`str`
93 Original title of the show (as suggested by the addon)
100 dlg = xbmcgui.Dialog()
101 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
103 def show_password_dialog (self):
104 """Asks the user for its Netflix password
111 dlg = xbmcgui.Dialog()
112 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
114 def show_email_dialog (self):
115 """Asks the user for its Netflix account email
120 Netflix account email
122 dlg = xbmcgui.Dialog()
123 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
125 def show_login_failed_notification (self):
126 """Shows notification that the login failed
133 dialog = xbmcgui.Dialog()
134 dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
137 def show_missing_inputstream_addon_notification (self):
138 """Shows notification that the inputstream addon couldn't be found
145 dialog = xbmcgui.Dialog()
146 dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
149 def show_no_search_results_notification (self):
150 """Shows notification that no search results could be found
157 dialog = xbmcgui.Dialog()
158 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
161 def show_no_seasons_notification (self):
162 """Shows notification that no seasons be found
169 dialog = xbmcgui.Dialog()
170 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
173 def set_setting (self, key, value):
174 """Public interface for the addons setSetting method
179 Setting could be set or not
181 return self.get_addon().setSetting(key, value)
183 def get_credentials (self):
184 """Returns the users stored credentials
188 :obj:`dict` of :obj:`str`
189 The users stored account data
192 'email': self.get_addon().getSetting('email'),
193 'password': self.get_addon().getSetting('password')
198 Returns the esn from settings
200 self.log(msg='Is FILE: ' + str(isfile(self.msl_data_path + 'msl_data.json')))
201 self.log(msg=self.get_addon().getSetting('esn'))
202 return self.get_addon().getSetting('esn')
204 def set_esn(self, esn):
206 Returns the esn from settings
208 stored_esn = self.get_esn()
209 if not stored_esn and esn:
210 self.set_setting('esn', esn)
211 self.delete_manifest_data()
215 def delete_manifest_data(self):
216 if isfile(self.msl_data_path + 'msl_data.json'):
217 remove(self.msl_data_path + 'msl_data.json')
218 if isfile(self.msl_data_path + 'manifest.json'):
219 remove(self.msl_data_path + 'manifest.json')
220 msl = MSL(kodi_helper=self)
221 msl.perform_key_handshake()
224 def get_dolby_setting(self):
226 Returns if the dolby sound is enabled
229 return self.get_addon().getSetting('enable_dolby_sound') == 'true'
231 def get_custom_library_settings (self):
232 """Returns the settings in regards to the custom library folder(s)
236 :obj:`dict` of :obj:`str`
237 The users library settings
240 'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
241 'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
244 def get_ssl_verification_setting (self):
245 """Returns the setting that describes if we should verify the ssl transport when loading data
252 return self.get_addon().getSetting('ssl_verification') == 'true'
254 def set_main_menu_selection (self, type):
255 """Persist the chosen main menu entry in memory
262 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
264 def get_main_menu_selection (self):
265 """Gets the persisted chosen main menu entry from memory
270 The last chosen main menu entry
272 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
274 def setup_memcache (self):
275 """Sets up the memory cache if not existant"""
276 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
277 # no cache setup yet, create one
278 if len(cached_items) < 1:
279 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
281 def invalidate_memcache (self):
282 """Invalidates the memory cache"""
283 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
285 def get_cached_item (self, cache_id):
286 """Returns an item from the in memory cache
290 cache_id : :obj:`str`
291 ID of the cache entry
296 Contents of the requested cache item or none
298 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
300 return cached_items.get(cache_id)
302 def add_cached_item (self, cache_id, contents):
303 """Adds an item to the in memory cache
307 cache_id : :obj:`str`
308 ID of the cache entry
313 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
314 cached_items.update({cache_id: contents})
315 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
317 def build_profiles_listing (self, profiles, action, build_url):
318 """Builds the profiles list Kodi screen
322 profiles : :obj:`dict` of :obj:`str`
323 List of user profiles
326 Action paramter to build the subsequent routes
328 build_url : :obj:`fn`
329 Function to build the subsequent routes
336 for profile_id in profiles:
337 profile = profiles[profile_id]
338 url = build_url({'action': action, 'profile_id': profile_id})
339 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
340 li.setProperty('fanart_image', self.default_fanart)
341 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
342 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
343 xbmcplugin.endOfDirectory(self.plugin_handle)
346 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
347 """Builds the video lists (my list, continue watching, etc.) Kodi screen
351 video_list_ids : :obj:`dict` of :obj:`str`
354 user_list_order : :obj:`list` of :obj:`str`
355 Ordered user lists, to determine what should be displayed in the main menue
357 actions : :obj:`dict` of :obj:`str`
358 Dictionary of actions to build subsequent routes
360 build_url : :obj:`fn`
361 Function to build the subsequent routes
369 for category in user_list_order:
370 for video_list_id in video_list_ids['user']:
371 if video_list_ids['user'][video_list_id]['name'] == category:
372 label = video_list_ids['user'][video_list_id]['displayName']
373 if category == 'netflixOriginals':
374 label = label.capitalize()
375 li = xbmcgui.ListItem(label=label)
376 li.setProperty('fanart_image', self.default_fanart)
377 # determine action route
378 action = actions['default']
379 if category in actions.keys():
380 action = actions[category]
381 # determine if the item should be selected
382 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
383 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
384 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
386 # add recommendations/genres as subfolders (save us some space on the home page)
388 'recommendations': self.get_local_string(30001),
389 'genres': self.get_local_string(30010)
391 for type in i18n_ids.keys():
392 # determine if the lists have contents
393 if len(video_list_ids[type]) > 0:
394 # determine action route
395 action = actions['default']
396 if type in actions.keys():
397 action = actions[type]
398 # determine if the item should be selected
399 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
400 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
401 li_rec.setProperty('fanart_image', self.default_fanart)
402 url_rec = build_url({'action': action, 'type': type})
403 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
405 # add search as subfolder
406 action = actions['default']
407 if 'search' in actions.keys():
408 action = actions[type]
409 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
410 li_rec.setProperty('fanart_image', self.default_fanart)
411 url_rec = build_url({'action': action, 'type': 'search'})
412 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
415 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
416 xbmcplugin.endOfDirectory(self.plugin_handle)
418 # (re)select the previously selected main menu entry
420 for item in preselect_items:
422 preselected_list_item = idx if item else None
423 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
424 if preselected_list_item != None:
425 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
428 def build_video_listing (self, video_list, actions, type, build_url):
429 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
433 video_list_ids : :obj:`dict` of :obj:`str`
436 actions : :obj:`dict` of :obj:`str`
437 Dictionary of actions to build subsequent routes
440 None or 'queue' f.e. when it´s a special video lists
442 build_url : :obj:`fn`
443 Function to build the subsequent routes
450 for video_list_id in video_list:
451 video = video_list[video_list_id]
452 li = xbmcgui.ListItem(label=video['title'])
453 # add some art to the item
454 li = self._generate_art_info(entry=video, li=li)
456 li, infos = self._generate_entry_info(entry=video, li=li)
457 li = self._generate_context_menu_items(entry=video, li=li)
458 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
459 if video_list[video_list_id]['type'] == 'movie':
460 # it´s a movie, so we need no subfolder & a route to play it
462 url = build_url({'action': 'play_video', 'video_id': video_list_id, 'infoLabels': infos})
464 # it´s a show, so we need a subfolder & route (for seasons)
466 params = {'action': actions[video['type']], 'show_id': video_list_id}
467 if 'tvshowtitle' in infos:
468 params['tvshowtitle'] = infos['tvshowtitle']
469 url = build_url(params)
470 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
472 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
473 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
474 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
475 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
476 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
477 xbmcplugin.endOfDirectory(self.plugin_handle)
480 def build_search_result_listing (self, video_list, actions, build_url):
481 """Builds the search results list Kodi screen
485 video_list : :obj:`dict` of :obj:`str`
486 List of videos or shows
488 actions : :obj:`dict` of :obj:`str`
489 Dictionary of actions to build subsequent routes
491 build_url : :obj:`fn`
492 Function to build the subsequent routes
499 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
501 def build_no_seasons_available (self):
502 """Builds the season list screen if no seasons could be found
509 self.show_no_seasons_notification()
510 xbmcplugin.endOfDirectory(self.plugin_handle)
513 def build_no_search_results_available (self, build_url, action):
514 """Builds the search results screen if no matches could be found
519 Action paramter to build the subsequent routes
521 build_url : :obj:`fn`
522 Function to build the subsequent routes
529 self.show_no_search_results_notification()
530 return xbmcplugin.endOfDirectory(self.plugin_handle)
532 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
533 """Builds the video lists screen for user subfolders (genres & recommendations)
537 video_list_ids : :obj:`dict` of :obj:`str`
541 List type (genre or recommendation)
544 Action paramter to build the subsequent routes
546 build_url : :obj:`fn`
547 Function to build the subsequent routes
554 for video_list_id in video_list_ids:
555 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
556 li.setProperty('fanart_image', self.default_fanart)
557 url = build_url({'action': action, 'video_list_id': video_list_id})
558 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
560 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
561 xbmcplugin.endOfDirectory(self.plugin_handle)
564 def build_season_listing (self, seasons_sorted, season_list, build_url):
565 """Builds the season list screen for a show
569 seasons_sorted : :obj:`list` of :obj:`str`
570 Sorted season indexes
572 season_list : :obj:`dict` of :obj:`str`
573 List of season entries
575 build_url : :obj:`fn`
576 Function to build the subsequent routes
583 for index in seasons_sorted:
584 for season_id in season_list:
585 season = season_list[season_id]
586 if int(season['idx']) == index:
587 li = xbmcgui.ListItem(label=season['text'])
588 # add some art to the item
589 li = self._generate_art_info(entry=season, li=li)
591 li, infos = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
592 li = self._generate_context_menu_items(entry=season, li=li)
593 params = {'action': 'episode_list', 'season_id': season_id}
594 if 'tvshowtitle' in infos:
595 params['tvshowtitle'] = infos['tvshowtitle']
596 url = build_url(params)
597 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
599 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
600 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
601 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
602 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
603 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
604 xbmcplugin.endOfDirectory(self.plugin_handle)
607 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
608 """Builds the episode list screen for a season of a show
612 episodes_sorted : :obj:`list` of :obj:`str`
613 Sorted episode indexes
615 episode_list : :obj:`dict` of :obj:`str`
616 List of episode entries
618 build_url : :obj:`fn`
619 Function to build the subsequent routes
626 for index in episodes_sorted:
627 for episode_id in episode_list:
628 episode = episode_list[episode_id]
629 if int(episode['episode']) == index:
630 li = xbmcgui.ListItem(label=episode['title'])
631 # add some art to the item
632 li = self._generate_art_info(entry=episode, li=li)
634 li, infos = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
635 li = self._generate_context_menu_items(entry=episode, li=li)
636 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark'], 'infoLabels': infos})
637 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
639 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
640 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
641 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
642 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
643 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
644 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
645 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
646 xbmcplugin.endOfDirectory(self.plugin_handle)
649 def play_item (self, esn, video_id, start_offset=-1, infoLabels={}):
655 ESN needed for Widevine/Inputstream
657 video_id : :obj:`str`
658 ID of the video that should be played
660 start_offset : :obj:`str`
661 Offset to resume playback from (in seconds)
663 infoLabels : :obj:`str`
664 the listitem's infoLabels
672 addon = self.get_addon()
673 inputstream_addon = self.get_inputstream_addon()
674 if inputstream_addon == None:
675 self.show_missing_inputstream_addon_notification()
676 self.log(msg='Inputstream addon not found')
680 self.track_event('playVideo')
682 # check esn in settings
683 settings_esn = str(addon.getSetting('esn'))
684 if len(settings_esn) == 0:
685 addon.setSetting('esn', str(esn))
687 # inputstream addon properties
688 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
689 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
690 play_item.setContentLookup(False)
691 play_item.setMimeType('application/dash+xml')
692 play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())
693 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
694 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
695 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
696 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=')
697 play_item.setProperty('inputstreamaddon', inputstream_addon)
699 # check if we have a bookmark e.g. start offset position
700 if int(start_offset) > 0:
701 play_item.setProperty('StartOffset', str(start_offset) + '.0')
703 if len(infoLabels) > 0:
704 play_item.setInfo('video', infoLabels)
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 elif entry['type'] == 'show':
801 infos.update({'tvshowtitle': entry['title']})
802 if 'mediatype' in entry_keys:
803 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
804 li.setProperty('IsPlayable', 'true')
805 infos.update({'mediatype': entry['mediatype']})
806 if 'watched' in entry_keys:
807 infos.update({'playcount': (1, 0)[entry['watched']]})
808 if 'index' in entry_keys:
809 infos.update({'episode': entry['index']})
810 if 'episode' in entry_keys:
811 infos.update({'episode': entry['episode']})
812 if 'year' in entry_keys:
813 infos.update({'year': entry['year']})
814 if 'quality' in entry_keys:
815 quality = {'width': '960', 'height': '540'}
816 if entry['quality'] == '720':
817 quality = {'width': '1280', 'height': '720'}
818 if entry['quality'] == '1080':
819 quality = {'width': '1920', 'height': '1080'}
820 li.addStreamInfo('video', quality)
821 if 'tvshowtitle' in entry_keys:
822 infos.update({'tvshowtitle': entry['tvshowtitle']})
823 li.setInfo('video', infos)
826 def _generate_context_menu_items (self, entry, li):
827 """Adds context menue items to a Kodi list item
831 entry : :obj:`dict` of :obj:`str`
832 Entry that should be turned into a list item
834 li : :obj:`XMBC.ListItem`
835 Kodi list item instance
839 Kodi list item instance
843 entry_keys = entry.keys()
845 # action item templates
846 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
847 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
849 ['export_to_library', self.get_local_string(30018), 'export'],
850 ['remove_from_library', self.get_local_string(30030), 'remove'],
851 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
852 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
853 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
856 # build concrete action items
857 for action_item in actions:
858 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
860 # add or remove the movie/show/season/episode from & to the users "My List"
861 if 'in_my_list' in entry_keys:
862 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
863 elif 'queue' in entry_keys:
864 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
865 elif 'my_list' in entry_keys:
866 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
867 # rate the movie/show/season/episode on Netflix
868 items.append(action['rate_on_netflix'])
870 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
871 if 'type' in entry_keys:
873 if entry['type'] == 'movie':
874 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
875 items.append(action[action_type])
877 if entry['type'] == 'show' and 'title' in entry_keys:
878 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
879 items.append(action[action_type])
882 li.addContextMenuItems(items)
885 def log (self, msg, level=xbmc.LOGDEBUG):
886 """Adds a log entry to the Kodi log
891 Entry that should be turned into a list item
896 if isinstance(msg, unicode):
897 msg = msg.encode('utf-8')
898 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
900 def get_local_string (self, string_id):
901 """Returns the localized version of a string
905 string_id : :obj:`int`
906 ID of the string that shoudl be fetched
911 Requested string or empty string
913 src = xbmc if string_id < 30000 else self.get_addon()
914 locString = src.getLocalizedString(string_id)
915 if isinstance(locString, unicode):
916 locString = locString.encode('utf-8')
919 def get_inputstream_addon (self):
920 """Checks if the inputstream addon is installed & enabled.
921 Returns the type of the inputstream addon used or None if not found
926 Inputstream addon or None
928 type = 'inputstream.adaptive'
932 'method': 'Addons.GetAddonDetails',
935 'properties': ['enabled']
938 response = xbmc.executeJSONRPC(json.dumps(payload))
939 data = json.loads(response)
940 if not 'error' in data.keys():
941 if data['result']['addon']['enabled'] == True:
945 def set_library (self, library):
946 """Adds an instance of the Library class
950 library : :obj:`Library`
951 instance of the Library class
953 self.library = library
955 def track_event(self, event):
957 Send a tracking event if tracking is enabled
958 :param event: the string idetifier of the event
961 addon = self.get_addon()
962 # Check if tracking is enabled
963 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
965 #Get or Create Tracking id
966 tracking_id = addon.getSetting('tracking_id')
967 if tracking_id is '':
968 tracking_id = str(uuid4())
969 addon.setSetting('tracking_id', tracking_id)
970 # Send the tracking event
971 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
972 tracker.send('event', event)