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)
472 # it´s a show, so we need a subfolder & route (for seasons)
474 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
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 li = self._generate_entry_info(entry=video, li=li)
482 li = self._generate_context_menu_items(entry=video, li=li)
483 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
485 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
486 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
487 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
488 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
489 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
490 xbmcplugin.endOfDirectory(self.plugin_handle)
493 def build_search_result_listing (self, video_list, actions, build_url):
494 """Builds the search results list Kodi screen
498 video_list : :obj:`dict` of :obj:`str`
499 List of videos or shows
501 actions : :obj:`dict` of :obj:`str`
502 Dictionary of actions to build subsequent routes
504 build_url : :obj:`fn`
505 Function to build the subsequent routes
512 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
514 def build_no_seasons_available (self):
515 """Builds the season list screen if no seasons could be found
522 self.show_no_seasons_notification()
523 xbmcplugin.endOfDirectory(self.plugin_handle)
526 def build_no_search_results_available (self, build_url, action):
527 """Builds the search results screen if no matches could be found
532 Action paramter to build the subsequent routes
534 build_url : :obj:`fn`
535 Function to build the subsequent routes
542 self.show_no_search_results_notification()
543 return xbmcplugin.endOfDirectory(self.plugin_handle)
545 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
546 """Builds the video lists screen for user subfolders (genres & recommendations)
550 video_list_ids : :obj:`dict` of :obj:`str`
554 List type (genre or recommendation)
557 Action paramter to build the subsequent routes
559 build_url : :obj:`fn`
560 Function to build the subsequent routes
567 for video_list_id in video_list_ids:
568 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
569 li.setProperty('fanart_image', self.default_fanart)
570 url = build_url({'action': action, 'video_list_id': video_list_id})
571 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
573 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
574 xbmcplugin.endOfDirectory(self.plugin_handle)
577 def build_season_listing (self, seasons_sorted, season_list, build_url):
578 """Builds the season list screen for a show
582 seasons_sorted : :obj:`list` of :obj:`str`
583 Sorted season indexes
585 season_list : :obj:`dict` of :obj:`str`
586 List of season entries
588 build_url : :obj:`fn`
589 Function to build the subsequent routes
596 for index in seasons_sorted:
597 for season_id in season_list:
598 season = season_list[season_id]
599 if int(season['idx']) == index:
600 li = xbmcgui.ListItem(label=season['text'])
601 # add some art to the item
602 li = self._generate_art_info(entry=season, li=li)
604 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
605 li = self._generate_context_menu_items(entry=season, li=li)
606 url = build_url({'action': 'episode_list', 'season_id': season_id})
607 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
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.endOfDirectory(self.plugin_handle)
617 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
618 """Builds the episode list screen for a season of a show
622 episodes_sorted : :obj:`list` of :obj:`str`
623 Sorted episode indexes
625 episode_list : :obj:`dict` of :obj:`str`
626 List of episode entries
628 build_url : :obj:`fn`
629 Function to build the subsequent routes
636 for index in episodes_sorted:
637 for episode_id in episode_list:
638 episode = episode_list[episode_id]
639 if int(episode['episode']) == index:
640 li = xbmcgui.ListItem(label=episode['title'])
641 # add some art to the item
642 li = self._generate_art_info(entry=episode, li=li)
644 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
645 li = self._generate_context_menu_items(entry=episode, li=li)
646 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
647 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
649 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
650 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
651 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
652 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
653 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
654 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
655 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
656 xbmcplugin.endOfDirectory(self.plugin_handle)
659 def play_item (self, esn, video_id, start_offset=-1):
665 ESN needed for Widevine/Inputstream
667 video_id : :obj:`str`
668 ID of the video that should be played
670 start_offset : :obj:`str`
671 Offset to resume playback from (in seconds)
679 addon = self.get_addon()
680 inputstream_addon = self.get_inputstream_addon()
681 if inputstream_addon == None:
682 self.show_missing_inputstream_addon_notification()
683 self.log(msg='Inputstream addon not found')
687 self.track_event('playVideo')
689 # check esn in settings
690 settings_esn = str(addon.getSetting('esn'))
691 if len(settings_esn) == 0:
692 addon.setSetting('esn', str(esn))
694 # inputstream addon properties
695 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
696 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
697 play_item.setContentLookup(False)
698 play_item.setMimeType('application/dash+xml')
699 play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())
700 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
701 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
702 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
703 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=')
704 play_item.setProperty('inputstreamaddon', inputstream_addon)
706 # check if we have a bookmark e.g. start offset position
707 if int(start_offset) > 0:
708 play_item.setProperty('StartOffset', str(start_offset) + '.0')
709 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
711 def _generate_art_info (self, entry, li):
712 """Adds the art info from an entry to a Kodi list item
716 entry : :obj:`dict` of :obj:`str`
717 Entry that should be turned into a list item
719 li : :obj:`XMBC.ListItem`
720 Kodi list item instance
725 Kodi list item instance
727 art = {'fanart': self.default_fanart}
728 if 'boxarts' in dict(entry).keys():
730 'poster': entry['boxarts']['big'],
731 'landscape': entry['boxarts']['big'],
732 'thumb': entry['boxarts']['small'],
733 'fanart': entry['boxarts']['big']
735 if 'interesting_moment' in dict(entry).keys():
737 'poster': entry['interesting_moment'],
738 'fanart': entry['interesting_moment']
740 if 'thumb' in dict(entry).keys():
741 art.update({'thumb': entry['thumb']})
742 if 'fanart' in dict(entry).keys():
743 art.update({'fanart': entry['fanart']})
744 if 'poster' in dict(entry).keys():
745 art.update({'poster': entry['poster']})
749 def _generate_entry_info (self, entry, li, base_info={}):
750 """Adds the item info from an entry to a Kodi list item
754 entry : :obj:`dict` of :obj:`str`
755 Entry that should be turned into a list item
757 li : :obj:`XMBC.ListItem`
758 Kodi list item instance
760 base_info : :obj:`dict` of :obj:`str`
761 Additional info that overrules the entry info
766 Kodi list item instance
769 entry_keys = entry.keys()
770 if 'cast' in entry_keys and len(entry['cast']) > 0:
771 infos.update({'cast': entry['cast']})
772 if 'creators' in entry_keys and len(entry['creators']) > 0:
773 infos.update({'writer': entry['creators'][0]})
774 if 'directors' in entry_keys and len(entry['directors']) > 0:
775 infos.update({'director': entry['directors'][0]})
776 if 'genres' in entry_keys and len(entry['genres']) > 0:
777 infos.update({'genre': entry['genres'][0]})
778 if 'maturity' in entry_keys:
779 if 'mpaa' in entry_keys:
780 infos.update({'mpaa': entry['mpaa']})
782 if entry.get('maturity', None) is not None:
783 if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
784 infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
785 if 'rating' in entry_keys:
786 infos.update({'rating': int(entry['rating']) * 2})
787 if 'synopsis' in entry_keys:
788 infos.update({'plot': entry['synopsis']})
789 if 'plot' in entry_keys:
790 infos.update({'plot': entry['plot']})
791 if 'runtime' in entry_keys:
792 infos.update({'duration': entry['runtime']})
793 if 'duration' in entry_keys:
794 infos.update({'duration': entry['duration']})
795 if 'seasons_label' in entry_keys:
796 infos.update({'season': entry['seasons_label']})
797 if 'season' in entry_keys:
798 infos.update({'season': entry['season']})
799 if 'title' in entry_keys:
800 infos.update({'title': entry['title']})
801 if 'type' in entry_keys:
802 if entry['type'] == 'movie' or entry['type'] == 'episode':
803 li.setProperty('IsPlayable', 'true')
804 if 'mediatype' in entry_keys:
805 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
806 li.setProperty('IsPlayable', 'true')
807 infos.update({'mediatype': entry['mediatype']})
808 if 'watched' in entry_keys:
809 infos.update({'playcount': (1, 0)[entry['watched']]})
810 if 'index' in entry_keys:
811 infos.update({'episode': entry['index']})
812 if 'episode' in entry_keys:
813 infos.update({'episode': entry['episode']})
814 if 'year' in entry_keys:
815 infos.update({'year': entry['year']})
816 if 'quality' in entry_keys:
817 quality = {'width': '960', 'height': '540'}
818 if entry['quality'] == '720':
819 quality = {'width': '1280', 'height': '720'}
820 if entry['quality'] == '1080':
821 quality = {'width': '1920', 'height': '1080'}
822 li.addStreamInfo('video', quality)
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)