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 has_cached_item (self, cache_id):
286 """Checks if the requested item is in memory cache
290 cache_id : :obj:`str`
291 ID of the cache entry
298 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
299 return cache_id in cached_items.keys()
301 def get_cached_item (self, cache_id):
302 """Returns an item from the in memory cache
306 cache_id : :obj:`str`
307 ID of the cache entry
312 Contents of the requested cache item or none
314 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
315 if self.has_cached_item(cache_id) != True:
317 return cached_items[cache_id]
319 def add_cached_item (self, cache_id, contents):
320 """Adds an item to the in memory cache
324 cache_id : :obj:`str`
325 ID of the cache entry
330 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
331 cached_items.update({cache_id: contents})
332 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
334 def build_profiles_listing (self, profiles, action, build_url):
335 """Builds the profiles list Kodi screen
339 profiles : :obj:`dict` of :obj:`str`
340 List of user profiles
343 Action paramter to build the subsequent routes
345 build_url : :obj:`fn`
346 Function to build the subsequent routes
353 for profile_id in profiles:
354 profile = profiles[profile_id]
355 url = build_url({'action': action, 'profile_id': profile_id})
356 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
357 li.setProperty('fanart_image', self.default_fanart)
358 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
359 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
360 xbmcplugin.endOfDirectory(self.plugin_handle)
363 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
364 """Builds the video lists (my list, continue watching, etc.) Kodi screen
368 video_list_ids : :obj:`dict` of :obj:`str`
371 user_list_order : :obj:`list` of :obj:`str`
372 Ordered user lists, to determine what should be displayed in the main menue
374 actions : :obj:`dict` of :obj:`str`
375 Dictionary of actions to build subsequent routes
377 build_url : :obj:`fn`
378 Function to build the subsequent routes
386 for category in user_list_order:
387 for video_list_id in video_list_ids['user']:
388 if video_list_ids['user'][video_list_id]['name'] == category:
389 label = video_list_ids['user'][video_list_id]['displayName']
390 if category == 'netflixOriginals':
391 label = label.capitalize()
392 li = xbmcgui.ListItem(label=label)
393 li.setProperty('fanart_image', self.default_fanart)
394 # determine action route
395 action = actions['default']
396 if category in actions.keys():
397 action = actions[category]
398 # determine if the item should be selected
399 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
400 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
401 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
403 # add recommendations/genres as subfolders (save us some space on the home page)
405 'recommendations': self.get_local_string(30001),
406 'genres': self.get_local_string(30010)
408 for type in i18n_ids.keys():
409 # determine if the lists have contents
410 if len(video_list_ids[type]) > 0:
411 # determine action route
412 action = actions['default']
413 if type in actions.keys():
414 action = actions[type]
415 # determine if the item should be selected
416 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
417 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
418 li_rec.setProperty('fanart_image', self.default_fanart)
419 url_rec = build_url({'action': action, 'type': type})
420 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
422 # add search as subfolder
423 action = actions['default']
424 if 'search' in actions.keys():
425 action = actions[type]
426 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
427 li_rec.setProperty('fanart_image', self.default_fanart)
428 url_rec = build_url({'action': action, 'type': 'search'})
429 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
432 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
433 xbmcplugin.endOfDirectory(self.plugin_handle)
435 # (re)select the previously selected main menu entry
437 for item in preselect_items:
439 preselected_list_item = idx if item else None
440 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
441 if preselected_list_item != None:
442 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
445 def build_video_listing (self, video_list, actions, type, build_url):
446 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
450 video_list_ids : :obj:`dict` of :obj:`str`
453 actions : :obj:`dict` of :obj:`str`
454 Dictionary of actions to build subsequent routes
457 None or 'queue' f.e. when it´s a special video lists
459 build_url : :obj:`fn`
460 Function to build the subsequent routes
467 for video_list_id in video_list:
468 video = video_list[video_list_id]
469 li = xbmcgui.ListItem(label=video['title'])
470 # add some art to the item
471 li = self._generate_art_info(entry=video, li=li)
473 li, infos = self._generate_entry_info(entry=video, li=li)
474 li = self._generate_context_menu_items(entry=video, li=li)
475 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
476 if video_list[video_list_id]['type'] == 'movie':
477 # it´s a movie, so we need no subfolder & a route to play it
479 url = build_url({'action': 'play_video', 'video_id': video_list_id})
481 # it´s a show, so we need a subfolder & route (for seasons)
483 params = {'action': actions[video['type']], 'show_id': video_list_id}
484 if 'tvshowtitle' in infos:
485 params['tvshowtitle'] = infos['tvshowtitle']
486 url = build_url(params)
487 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
489 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
490 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
491 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
492 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
493 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
494 xbmcplugin.endOfDirectory(self.plugin_handle)
497 def build_search_result_listing (self, video_list, actions, build_url):
498 """Builds the search results list Kodi screen
502 video_list : :obj:`dict` of :obj:`str`
503 List of videos or shows
505 actions : :obj:`dict` of :obj:`str`
506 Dictionary of actions to build subsequent routes
508 build_url : :obj:`fn`
509 Function to build the subsequent routes
516 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
518 def build_no_seasons_available (self):
519 """Builds the season list screen if no seasons could be found
526 self.show_no_seasons_notification()
527 xbmcplugin.endOfDirectory(self.plugin_handle)
530 def build_no_search_results_available (self, build_url, action):
531 """Builds the search results screen if no matches could be found
536 Action paramter to build the subsequent routes
538 build_url : :obj:`fn`
539 Function to build the subsequent routes
546 self.show_no_search_results_notification()
547 return xbmcplugin.endOfDirectory(self.plugin_handle)
549 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
550 """Builds the video lists screen for user subfolders (genres & recommendations)
554 video_list_ids : :obj:`dict` of :obj:`str`
558 List type (genre or recommendation)
561 Action paramter to build the subsequent routes
563 build_url : :obj:`fn`
564 Function to build the subsequent routes
571 for video_list_id in video_list_ids:
572 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
573 li.setProperty('fanart_image', self.default_fanart)
574 url = build_url({'action': action, 'video_list_id': video_list_id})
575 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
577 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
578 xbmcplugin.endOfDirectory(self.plugin_handle)
581 def build_season_listing (self, seasons_sorted, season_list, build_url):
582 """Builds the season list screen for a show
586 seasons_sorted : :obj:`list` of :obj:`str`
587 Sorted season indexes
589 season_list : :obj:`dict` of :obj:`str`
590 List of season entries
592 build_url : :obj:`fn`
593 Function to build the subsequent routes
600 for index in seasons_sorted:
601 for season_id in season_list:
602 season = season_list[season_id]
603 if int(season['idx']) == index:
604 li = xbmcgui.ListItem(label=season['text'])
605 # add some art to the item
606 li = self._generate_art_info(entry=season, li=li)
608 li, infos = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
609 li = self._generate_context_menu_items(entry=season, li=li)
610 params = {'action': 'episode_list', 'season_id': season_id}
611 if 'tvshowtitle' in infos:
612 params['tvshowtitle'] = infos['tvshowtitle']
613 url = build_url(params)
614 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
616 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
617 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
618 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
619 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
620 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
621 xbmcplugin.endOfDirectory(self.plugin_handle)
624 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
625 """Builds the episode list screen for a season of a show
629 episodes_sorted : :obj:`list` of :obj:`str`
630 Sorted episode indexes
632 episode_list : :obj:`dict` of :obj:`str`
633 List of episode entries
635 build_url : :obj:`fn`
636 Function to build the subsequent routes
643 for index in episodes_sorted:
644 for episode_id in episode_list:
645 episode = episode_list[episode_id]
646 if int(episode['episode']) == index:
647 li = xbmcgui.ListItem(label=episode['title'])
648 # add some art to the item
649 li = self._generate_art_info(entry=episode, li=li)
651 li, infos = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
652 li = self._generate_context_menu_items(entry=episode, li=li)
653 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
654 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
656 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
657 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
658 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
659 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
660 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
661 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
662 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
663 xbmcplugin.endOfDirectory(self.plugin_handle)
666 def play_item (self, esn, video_id, start_offset=-1):
672 ESN needed for Widevine/Inputstream
674 video_id : :obj:`str`
675 ID of the video that should be played
677 start_offset : :obj:`str`
678 Offset to resume playback from (in seconds)
686 addon = self.get_addon()
687 inputstream_addon = self.get_inputstream_addon()
688 if inputstream_addon == None:
689 self.show_missing_inputstream_addon_notification()
690 self.log(msg='Inputstream addon not found')
694 self.track_event('playVideo')
696 # check esn in settings
697 settings_esn = str(addon.getSetting('esn'))
698 if len(settings_esn) == 0:
699 addon.setSetting('esn', str(esn))
701 # inputstream addon properties
702 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
703 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
704 play_item.setContentLookup(False)
705 play_item.setMimeType('application/dash+xml')
706 play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())
707 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
708 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
709 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
710 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=')
711 play_item.setProperty('inputstreamaddon', inputstream_addon)
713 # check if we have a bookmark e.g. start offset position
714 if int(start_offset) > 0:
715 play_item.setProperty('StartOffset', str(start_offset) + '.0')
716 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
718 def _generate_art_info (self, entry, li):
719 """Adds the art info from an entry to a Kodi list item
723 entry : :obj:`dict` of :obj:`str`
724 Entry that should be turned into a list item
726 li : :obj:`XMBC.ListItem`
727 Kodi list item instance
732 Kodi list item instance
734 art = {'fanart': self.default_fanart}
735 if 'boxarts' in dict(entry).keys():
737 'poster': entry['boxarts']['big'],
738 'landscape': entry['boxarts']['big'],
739 'thumb': entry['boxarts']['small'],
740 'fanart': entry['boxarts']['big']
742 if 'interesting_moment' in dict(entry).keys():
744 'poster': entry['interesting_moment'],
745 'fanart': entry['interesting_moment']
747 if 'thumb' in dict(entry).keys():
748 art.update({'thumb': entry['thumb']})
749 if 'fanart' in dict(entry).keys():
750 art.update({'fanart': entry['fanart']})
751 if 'poster' in dict(entry).keys():
752 art.update({'poster': entry['poster']})
756 def _generate_entry_info (self, entry, li, base_info={}):
757 """Adds the item info from an entry to a Kodi list item
761 entry : :obj:`dict` of :obj:`str`
762 Entry that should be turned into a list item
764 li : :obj:`XMBC.ListItem`
765 Kodi list item instance
767 base_info : :obj:`dict` of :obj:`str`
768 Additional info that overrules the entry info
773 Kodi list item instance
776 entry_keys = entry.keys()
777 if 'cast' in entry_keys and len(entry['cast']) > 0:
778 infos.update({'cast': entry['cast']})
779 if 'creators' in entry_keys and len(entry['creators']) > 0:
780 infos.update({'writer': entry['creators'][0]})
781 if 'directors' in entry_keys and len(entry['directors']) > 0:
782 infos.update({'director': entry['directors'][0]})
783 if 'genres' in entry_keys and len(entry['genres']) > 0:
784 infos.update({'genre': entry['genres'][0]})
785 if 'maturity' in entry_keys:
786 if 'mpaa' in entry_keys:
787 infos.update({'mpaa': entry['mpaa']})
789 if entry.get('maturity', None) is not None:
790 if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
791 infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
792 if 'rating' in entry_keys:
793 infos.update({'rating': int(entry['rating']) * 2})
794 if 'synopsis' in entry_keys:
795 infos.update({'plot': entry['synopsis']})
796 if 'plot' in entry_keys:
797 infos.update({'plot': entry['plot']})
798 if 'runtime' in entry_keys:
799 infos.update({'duration': entry['runtime']})
800 if 'duration' in entry_keys:
801 infos.update({'duration': entry['duration']})
802 if 'seasons_label' in entry_keys:
803 infos.update({'season': entry['seasons_label']})
804 if 'season' in entry_keys:
805 infos.update({'season': entry['season']})
806 if 'title' in entry_keys:
807 infos.update({'title': entry['title']})
808 if 'type' in entry_keys:
809 if entry['type'] == 'movie' or entry['type'] == 'episode':
810 li.setProperty('IsPlayable', 'true')
811 elif entry['type'] == 'show':
812 infos.update({'tvshowtitle': entry['title']})
813 if 'mediatype' in entry_keys:
814 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
815 li.setProperty('IsPlayable', 'true')
816 infos.update({'mediatype': entry['mediatype']})
817 if 'watched' in entry_keys:
818 infos.update({'playcount': (1, 0)[entry['watched']]})
819 if 'index' in entry_keys:
820 infos.update({'episode': entry['index']})
821 if 'episode' in entry_keys:
822 infos.update({'episode': entry['episode']})
823 if 'year' in entry_keys:
824 infos.update({'year': entry['year']})
825 if 'quality' in entry_keys:
826 quality = {'width': '960', 'height': '540'}
827 if entry['quality'] == '720':
828 quality = {'width': '1280', 'height': '720'}
829 if entry['quality'] == '1080':
830 quality = {'width': '1920', 'height': '1080'}
831 li.addStreamInfo('video', quality)
832 if 'tvshowtitle' in entry_keys:
833 infos.update({'tvshowtitle': entry['tvshowtitle']})
834 li.setInfo('video', infos)
837 def _generate_context_menu_items (self, entry, li):
838 """Adds context menue items to a Kodi list item
842 entry : :obj:`dict` of :obj:`str`
843 Entry that should be turned into a list item
845 li : :obj:`XMBC.ListItem`
846 Kodi list item instance
850 Kodi list item instance
854 entry_keys = entry.keys()
856 # action item templates
857 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
858 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
860 ['export_to_library', self.get_local_string(30018), 'export'],
861 ['remove_from_library', self.get_local_string(30030), 'remove'],
862 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
863 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
864 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
867 # build concrete action items
868 for action_item in actions:
869 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
871 # add or remove the movie/show/season/episode from & to the users "My List"
872 if 'in_my_list' in entry_keys:
873 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
874 elif 'queue' in entry_keys:
875 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
876 elif 'my_list' in entry_keys:
877 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
878 # rate the movie/show/season/episode on Netflix
879 items.append(action['rate_on_netflix'])
881 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
882 if 'type' in entry_keys:
884 if entry['type'] == 'movie':
885 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
886 items.append(action[action_type])
888 if entry['type'] == 'show' and 'title' in entry_keys:
889 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
890 items.append(action[action_type])
893 li.addContextMenuItems(items)
896 def log (self, msg, level=xbmc.LOGDEBUG):
897 """Adds a log entry to the Kodi log
902 Entry that should be turned into a list item
907 if isinstance(msg, unicode):
908 msg = msg.encode('utf-8')
909 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
911 def get_local_string (self, string_id):
912 """Returns the localized version of a string
916 string_id : :obj:`int`
917 ID of the string that shoudl be fetched
922 Requested string or empty string
924 src = xbmc if string_id < 30000 else self.get_addon()
925 locString = src.getLocalizedString(string_id)
926 if isinstance(locString, unicode):
927 locString = locString.encode('utf-8')
930 def get_inputstream_addon (self):
931 """Checks if the inputstream addon is installed & enabled.
932 Returns the type of the inputstream addon used or None if not found
937 Inputstream addon or None
939 type = 'inputstream.adaptive'
943 'method': 'Addons.GetAddonDetails',
946 'properties': ['enabled']
949 response = xbmc.executeJSONRPC(json.dumps(payload))
950 data = json.loads(response)
951 if not 'error' in data.keys():
952 if data['result']['addon']['enabled'] == True:
956 def set_library (self, library):
957 """Adds an instance of the Library class
961 library : :obj:`Library`
962 instance of the Library class
964 self.library = library
966 def track_event(self, event):
968 Send a tracking event if tracking is enabled
969 :param event: the string idetifier of the event
972 addon = self.get_addon()
973 # Check if tracking is enabled
974 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
976 #Get or Create Tracking id
977 tracking_id = addon.getSetting('tracking_id')
978 if tracking_id is '':
979 tracking_id = str(uuid4())
980 addon.setSetting('tracking_id', tracking_id)
981 # Send the tracking event
982 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
983 tracker.send('event', event)