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 return self.get_addon().getSetting('esn')
201 def set_esn(self, esn):
203 Returns the esn from settings
205 stored_esn = self.get_esn()
207 self.set_setting('esn', esn)
208 self.delete_manifest_data()
212 def delete_manifest_data(self):
213 if isfile(self.msl_data_path + 'msl_data.json'):
214 remove(self.msl_data_path + 'msl_data.json')
215 if isfile(self.msl_data_path + 'manifest.json'):
216 remove(self.msl_data_path + 'manifest.json')
217 msl = MSL(kodi_helper=self)
220 def get_dolby_setting(self):
222 Returns if the dolby sound is enabled
225 return self.get_addon().getSetting('enable_dolby_sound') == 'true'
227 def get_custom_library_settings (self):
228 """Returns the settings in regards to the custom library folder(s)
232 :obj:`dict` of :obj:`str`
233 The users library settings
236 'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
237 'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
240 def get_ssl_verification_setting (self):
241 """Returns the setting that describes if we should verify the ssl transport when loading data
248 return self.get_addon().getSetting('ssl_verification') == 'true'
250 def set_main_menu_selection (self, type):
251 """Persist the chosen main menu entry in memory
258 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
260 def get_main_menu_selection (self):
261 """Gets the persisted chosen main menu entry from memory
266 The last chosen main menu entry
268 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
270 def setup_memcache (self):
271 """Sets up the memory cache if not existant"""
272 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
273 # no cache setup yet, create one
274 if len(cached_items) < 1:
275 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
277 def invalidate_memcache (self):
278 """Invalidates the memory cache"""
279 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
281 def has_cached_item (self, cache_id):
282 """Checks if the requested item is in memory cache
286 cache_id : :obj:`str`
287 ID of the cache entry
294 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
295 return cache_id in cached_items.keys()
297 def get_cached_item (self, cache_id):
298 """Returns an item from the in memory cache
302 cache_id : :obj:`str`
303 ID of the cache entry
308 Contents of the requested cache item or none
310 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
311 if self.has_cached_item(cache_id) != True:
313 return cached_items[cache_id]
315 def add_cached_item (self, cache_id, contents):
316 """Adds an item to the in memory cache
320 cache_id : :obj:`str`
321 ID of the cache entry
326 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
327 cached_items.update({cache_id: contents})
328 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
330 def build_profiles_listing (self, profiles, action, build_url):
331 """Builds the profiles list Kodi screen
335 profiles : :obj:`dict` of :obj:`str`
336 List of user profiles
339 Action paramter to build the subsequent routes
341 build_url : :obj:`fn`
342 Function to build the subsequent routes
349 for profile_id in profiles:
350 profile = profiles[profile_id]
351 url = build_url({'action': action, 'profile_id': profile_id})
352 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
353 li.setProperty('fanart_image', self.default_fanart)
354 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
355 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
356 xbmcplugin.endOfDirectory(self.plugin_handle)
359 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
360 """Builds the video lists (my list, continue watching, etc.) Kodi screen
364 video_list_ids : :obj:`dict` of :obj:`str`
367 user_list_order : :obj:`list` of :obj:`str`
368 Ordered user lists, to determine what should be displayed in the main menue
370 actions : :obj:`dict` of :obj:`str`
371 Dictionary of actions to build subsequent routes
373 build_url : :obj:`fn`
374 Function to build the subsequent routes
382 for category in user_list_order:
383 for video_list_id in video_list_ids['user']:
384 if video_list_ids['user'][video_list_id]['name'] == category:
385 label = video_list_ids['user'][video_list_id]['displayName']
386 if category == 'netflixOriginals':
387 label = label.capitalize()
388 li = xbmcgui.ListItem(label=label)
389 li.setProperty('fanart_image', self.default_fanart)
390 # determine action route
391 action = actions['default']
392 if category in actions.keys():
393 action = actions[category]
394 # determine if the item should be selected
395 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
396 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
397 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
399 # add recommendations/genres as subfolders (save us some space on the home page)
401 'recommendations': self.get_local_string(30001),
402 'genres': self.get_local_string(30010)
404 for type in i18n_ids.keys():
405 # determine if the lists have contents
406 if len(video_list_ids[type]) > 0:
407 # determine action route
408 action = actions['default']
409 if type in actions.keys():
410 action = actions[type]
411 # determine if the item should be selected
412 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
413 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
414 li_rec.setProperty('fanart_image', self.default_fanart)
415 url_rec = build_url({'action': action, 'type': type})
416 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
418 # add search as subfolder
419 action = actions['default']
420 if 'search' in actions.keys():
421 action = actions[type]
422 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
423 li_rec.setProperty('fanart_image', self.default_fanart)
424 url_rec = build_url({'action': action, 'type': 'search'})
425 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
428 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
429 xbmcplugin.endOfDirectory(self.plugin_handle)
431 # (re)select the previously selected main menu entry
433 for item in preselect_items:
435 preselected_list_item = idx if item else None
436 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
437 if preselected_list_item != None:
438 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
441 def build_video_listing (self, video_list, actions, type, build_url):
442 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
446 video_list_ids : :obj:`dict` of :obj:`str`
449 actions : :obj:`dict` of :obj:`str`
450 Dictionary of actions to build subsequent routes
453 None or 'queue' f.e. when it´s a special video lists
455 build_url : :obj:`fn`
456 Function to build the subsequent routes
463 for video_list_id in video_list:
464 video = video_list[video_list_id]
465 li = xbmcgui.ListItem(label=video['title'])
466 # add some art to the item
467 li = self._generate_art_info(entry=video, li=li)
468 # it´s a show, so we need a subfolder & route (for seasons)
470 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
471 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
472 if video_list[video_list_id]['type'] == 'movie':
473 # it´s a movie, so we need no subfolder & a route to play it
475 url = build_url({'action': 'play_video', 'video_id': video_list_id})
477 li = self._generate_entry_info(entry=video, li=li)
478 li = self._generate_context_menu_items(entry=video, li=li)
479 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
481 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
482 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
483 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
484 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
485 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
486 xbmcplugin.endOfDirectory(self.plugin_handle)
489 def build_search_result_listing (self, video_list, actions, build_url):
490 """Builds the search results list Kodi screen
494 video_list : :obj:`dict` of :obj:`str`
495 List of videos or shows
497 actions : :obj:`dict` of :obj:`str`
498 Dictionary of actions to build subsequent routes
500 build_url : :obj:`fn`
501 Function to build the subsequent routes
508 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
510 def build_no_seasons_available (self):
511 """Builds the season list screen if no seasons could be found
518 self.show_no_seasons_notification()
519 xbmcplugin.endOfDirectory(self.plugin_handle)
522 def build_no_search_results_available (self, build_url, action):
523 """Builds the search results screen if no matches could be found
528 Action paramter to build the subsequent routes
530 build_url : :obj:`fn`
531 Function to build the subsequent routes
538 self.show_no_search_results_notification()
539 return xbmcplugin.endOfDirectory(self.plugin_handle)
541 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
542 """Builds the video lists screen for user subfolders (genres & recommendations)
546 video_list_ids : :obj:`dict` of :obj:`str`
550 List type (genre or recommendation)
553 Action paramter to build the subsequent routes
555 build_url : :obj:`fn`
556 Function to build the subsequent routes
563 for video_list_id in video_list_ids:
564 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
565 li.setProperty('fanart_image', self.default_fanart)
566 url = build_url({'action': action, 'video_list_id': video_list_id})
567 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
569 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
570 xbmcplugin.endOfDirectory(self.plugin_handle)
573 def build_season_listing (self, seasons_sorted, season_list, build_url):
574 """Builds the season list screen for a show
578 seasons_sorted : :obj:`list` of :obj:`str`
579 Sorted season indexes
581 season_list : :obj:`dict` of :obj:`str`
582 List of season entries
584 build_url : :obj:`fn`
585 Function to build the subsequent routes
592 for index in seasons_sorted:
593 for season_id in season_list:
594 season = season_list[season_id]
595 if int(season['idx']) == index:
596 li = xbmcgui.ListItem(label=season['text'])
597 # add some art to the item
598 li = self._generate_art_info(entry=season, li=li)
600 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
601 li = self._generate_context_menu_items(entry=season, li=li)
602 url = build_url({'action': 'episode_list', 'season_id': season_id})
603 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
605 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
606 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
607 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
608 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
609 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
610 xbmcplugin.endOfDirectory(self.plugin_handle)
613 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
614 """Builds the episode list screen for a season of a show
618 episodes_sorted : :obj:`list` of :obj:`str`
619 Sorted episode indexes
621 episode_list : :obj:`dict` of :obj:`str`
622 List of episode entries
624 build_url : :obj:`fn`
625 Function to build the subsequent routes
632 for index in episodes_sorted:
633 for episode_id in episode_list:
634 episode = episode_list[episode_id]
635 if int(episode['episode']) == index:
636 li = xbmcgui.ListItem(label=episode['title'])
637 # add some art to the item
638 li = self._generate_art_info(entry=episode, li=li)
640 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
641 li = self._generate_context_menu_items(entry=episode, li=li)
642 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
643 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
645 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
646 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
647 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
648 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
649 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
650 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
651 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
652 xbmcplugin.endOfDirectory(self.plugin_handle)
655 def play_item (self, esn, video_id, start_offset=-1):
661 ESN needed for Widevine/Inputstream
663 video_id : :obj:`str`
664 ID of the video that should be played
666 start_offset : :obj:`str`
667 Offset to resume playback from (in seconds)
674 addon = self.get_addon()
675 inputstream_addon = self.get_inputstream_addon()
676 if inputstream_addon == None:
677 self.show_missing_inputstream_addon_notification()
678 self.log(msg='Inputstream addon not found')
682 self.track_event('playVideo')
684 # check esn in settings
685 settings_esn = str(addon.getSetting('esn'))
686 if len(settings_esn) == 0:
687 addon.setSetting('esn', str(esn))
689 # inputstream addon properties
690 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
691 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
692 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
693 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
694 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
695 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=')
696 play_item.setProperty('inputstreamaddon', inputstream_addon)
698 # check if we have a bookmark e.g. start offset position
699 if int(start_offset) > 0:
700 play_item.setProperty('StartOffset', str(start_offset) + '.0')
701 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
703 def _generate_art_info (self, entry, li):
704 """Adds the art info from an entry to a Kodi list item
708 entry : :obj:`dict` of :obj:`str`
709 Entry that should be turned into a list item
711 li : :obj:`XMBC.ListItem`
712 Kodi list item instance
717 Kodi list item instance
719 art = {'fanart': self.default_fanart}
720 if 'boxarts' in dict(entry).keys():
722 'poster': entry['boxarts']['big'],
723 'landscape': entry['boxarts']['big'],
724 'thumb': entry['boxarts']['small'],
725 'fanart': entry['boxarts']['big']
727 if 'interesting_moment' in dict(entry).keys():
729 'poster': entry['interesting_moment'],
730 'fanart': entry['interesting_moment']
732 if 'thumb' in dict(entry).keys():
733 art.update({'thumb': entry['thumb']})
734 if 'fanart' in dict(entry).keys():
735 art.update({'fanart': entry['fanart']})
736 if 'poster' in dict(entry).keys():
737 art.update({'poster': entry['poster']})
741 def _generate_entry_info (self, entry, li, base_info={}):
742 """Adds the item info from an entry to a Kodi list item
746 entry : :obj:`dict` of :obj:`str`
747 Entry that should be turned into a list item
749 li : :obj:`XMBC.ListItem`
750 Kodi list item instance
752 base_info : :obj:`dict` of :obj:`str`
753 Additional info that overrules the entry info
758 Kodi list item instance
761 entry_keys = entry.keys()
762 if 'cast' in entry_keys and len(entry['cast']) > 0:
763 infos.update({'cast': entry['cast']})
764 if 'creators' in entry_keys and len(entry['creators']) > 0:
765 infos.update({'writer': entry['creators'][0]})
766 if 'directors' in entry_keys and len(entry['directors']) > 0:
767 infos.update({'director': entry['directors'][0]})
768 if 'genres' in entry_keys and len(entry['genres']) > 0:
769 infos.update({'genre': entry['genres'][0]})
770 if 'maturity' in entry_keys:
771 if 'mpaa' in entry_keys:
772 infos.update({'mpaa': entry['mpaa']})
774 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
775 if 'rating' in entry_keys:
776 infos.update({'rating': int(entry['rating']) * 2})
777 if 'synopsis' in entry_keys:
778 infos.update({'plot': entry['synopsis']})
779 if 'plot' in entry_keys:
780 infos.update({'plot': entry['plot']})
781 if 'runtime' in entry_keys:
782 infos.update({'duration': entry['runtime']})
783 if 'duration' in entry_keys:
784 infos.update({'duration': entry['duration']})
785 if 'seasons_label' in entry_keys:
786 infos.update({'season': entry['seasons_label']})
787 if 'season' in entry_keys:
788 infos.update({'season': entry['season']})
789 if 'title' in entry_keys:
790 infos.update({'title': entry['title']})
791 if 'type' in entry_keys:
792 if entry['type'] == 'movie' or entry['type'] == 'episode':
793 li.setProperty('IsPlayable', 'true')
794 if 'mediatype' in entry_keys:
795 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
796 li.setProperty('IsPlayable', 'true')
797 infos.update({'mediatype': entry['mediatype']})
798 if 'watched' in entry_keys:
799 infos.update({'playcount': (1, 0)[entry['watched']]})
800 if 'index' in entry_keys:
801 infos.update({'episode': entry['index']})
802 if 'episode' in entry_keys:
803 infos.update({'episode': entry['episode']})
804 if 'year' in entry_keys:
805 infos.update({'year': entry['year']})
806 if 'quality' in entry_keys:
807 quality = {'width': '960', 'height': '540'}
808 if entry['quality'] == '720':
809 quality = {'width': '1280', 'height': '720'}
810 if entry['quality'] == '1080':
811 quality = {'width': '1920', 'height': '1080'}
812 li.addStreamInfo('video', quality)
813 li.setInfo('video', infos)
816 def _generate_context_menu_items (self, entry, li):
817 """Adds context menue items to a Kodi list item
821 entry : :obj:`dict` of :obj:`str`
822 Entry that should be turned into a list item
824 li : :obj:`XMBC.ListItem`
825 Kodi list item instance
829 Kodi list item instance
833 entry_keys = entry.keys()
835 # action item templates
836 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
837 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
839 ['export_to_library', self.get_local_string(30018), 'export'],
840 ['remove_from_library', self.get_local_string(30030), 'remove'],
841 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
842 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
843 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
846 # build concrete action items
847 for action_item in actions:
848 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
850 # add or remove the movie/show/season/episode from & to the users "My List"
851 if 'in_my_list' in entry_keys:
852 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
853 elif 'queue' in entry_keys:
854 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
855 elif 'my_list' in entry_keys:
856 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
857 # rate the movie/show/season/episode on Netflix
858 items.append(action['rate_on_netflix'])
860 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
861 if 'type' in entry_keys:
863 if entry['type'] == 'movie':
864 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
865 items.append(action[action_type])
867 if entry['type'] == 'show' and 'title' in entry_keys:
868 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
869 items.append(action[action_type])
872 li.addContextMenuItems(items)
875 def log (self, msg, level=xbmc.LOGDEBUG):
876 """Adds a log entry to the Kodi log
881 Entry that should be turned into a list item
886 if isinstance(msg, unicode):
887 msg = msg.encode('utf-8')
888 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
890 def get_local_string (self, string_id):
891 """Returns the localized version of a string
895 string_id : :obj:`int`
896 ID of the string that shoudl be fetched
901 Requested string or empty string
903 src = xbmc if string_id < 30000 else self.get_addon()
904 locString = src.getLocalizedString(string_id)
905 if isinstance(locString, unicode):
906 locString = locString.encode('utf-8')
909 def get_inputstream_addon (self):
910 """Checks if the inputstream addon is installed & enabled.
911 Returns the type of the inputstream addon used or None if not found
916 Inputstream addon or None
918 type = 'inputstream.adaptive'
922 'method': 'Addons.GetAddonDetails',
925 'properties': ['enabled']
928 response = xbmc.executeJSONRPC(json.dumps(payload))
929 data = json.loads(response)
930 if not 'error' in data.keys():
931 if data['result']['addon']['enabled'] == True:
935 def set_library (self, library):
936 """Adds an instance of the Library class
940 library : :obj:`Library`
941 instance of the Library class
943 self.library = library
945 def track_event(self, event):
947 Send a tracking event if tracking is enabled
948 :param event: the string idetifier of the event
951 addon = self.get_addon()
952 # Check if tracking is enabled
953 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
955 #Get or Create Tracking id
956 tracking_id = addon.getSetting('tracking_id')
957 if tracking_id is '':
958 tracking_id = str(uuid4())
959 addon.setSetting('tracking_id', tracking_id)
960 # Send the tracking event
961 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
962 tracker.send('event', event)