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, 'infoLabels': infos})
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'], 'infoLabels': infos})
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, infoLabels={}):
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)
680 infoLabels : :obj:`str`
681 the listitem's infoLabels
689 addon = self.get_addon()
690 inputstream_addon = self.get_inputstream_addon()
691 if inputstream_addon == None:
692 self.show_missing_inputstream_addon_notification()
693 self.log(msg='Inputstream addon not found')
697 self.track_event('playVideo')
699 # check esn in settings
700 settings_esn = str(addon.getSetting('esn'))
701 if len(settings_esn) == 0:
702 addon.setSetting('esn', str(esn))
704 # inputstream addon properties
705 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
706 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
707 play_item.setContentLookup(False)
708 play_item.setMimeType('application/dash+xml')
709 play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())
710 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
711 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
712 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
713 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=')
714 play_item.setProperty('inputstreamaddon', inputstream_addon)
716 # check if we have a bookmark e.g. start offset position
717 if int(start_offset) > 0:
718 play_item.setProperty('StartOffset', str(start_offset) + '.0')
720 if len(infoLabels) > 0:
721 play_item.setInfo('video', infoLabels)
722 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
724 def _generate_art_info (self, entry, li):
725 """Adds the art info from an entry to a Kodi list item
729 entry : :obj:`dict` of :obj:`str`
730 Entry that should be turned into a list item
732 li : :obj:`XMBC.ListItem`
733 Kodi list item instance
738 Kodi list item instance
740 art = {'fanart': self.default_fanart}
741 if 'boxarts' in dict(entry).keys():
743 'poster': entry['boxarts']['big'],
744 'landscape': entry['boxarts']['big'],
745 'thumb': entry['boxarts']['small'],
746 'fanart': entry['boxarts']['big']
748 if 'interesting_moment' in dict(entry).keys():
750 'poster': entry['interesting_moment'],
751 'fanart': entry['interesting_moment']
753 if 'thumb' in dict(entry).keys():
754 art.update({'thumb': entry['thumb']})
755 if 'fanart' in dict(entry).keys():
756 art.update({'fanart': entry['fanart']})
757 if 'poster' in dict(entry).keys():
758 art.update({'poster': entry['poster']})
762 def _generate_entry_info (self, entry, li, base_info={}):
763 """Adds the item info from an entry to a Kodi list item
767 entry : :obj:`dict` of :obj:`str`
768 Entry that should be turned into a list item
770 li : :obj:`XMBC.ListItem`
771 Kodi list item instance
773 base_info : :obj:`dict` of :obj:`str`
774 Additional info that overrules the entry info
779 Kodi list item instance
782 entry_keys = entry.keys()
783 if 'cast' in entry_keys and len(entry['cast']) > 0:
784 infos.update({'cast': entry['cast']})
785 if 'creators' in entry_keys and len(entry['creators']) > 0:
786 infos.update({'writer': entry['creators'][0]})
787 if 'directors' in entry_keys and len(entry['directors']) > 0:
788 infos.update({'director': entry['directors'][0]})
789 if 'genres' in entry_keys and len(entry['genres']) > 0:
790 infos.update({'genre': entry['genres'][0]})
791 if 'maturity' in entry_keys:
792 if 'mpaa' in entry_keys:
793 infos.update({'mpaa': entry['mpaa']})
795 if entry.get('maturity', None) is not None:
796 if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
797 infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
798 if 'rating' in entry_keys:
799 infos.update({'rating': int(entry['rating']) * 2})
800 if 'synopsis' in entry_keys:
801 infos.update({'plot': entry['synopsis']})
802 if 'plot' in entry_keys:
803 infos.update({'plot': entry['plot']})
804 if 'runtime' in entry_keys:
805 infos.update({'duration': entry['runtime']})
806 if 'duration' in entry_keys:
807 infos.update({'duration': entry['duration']})
808 if 'seasons_label' in entry_keys:
809 infos.update({'season': entry['seasons_label']})
810 if 'season' in entry_keys:
811 infos.update({'season': entry['season']})
812 if 'title' in entry_keys:
813 infos.update({'title': entry['title']})
814 if 'type' in entry_keys:
815 if entry['type'] == 'movie' or entry['type'] == 'episode':
816 li.setProperty('IsPlayable', 'true')
817 elif entry['type'] == 'show':
818 infos.update({'tvshowtitle': entry['title']})
819 if 'mediatype' in entry_keys:
820 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
821 li.setProperty('IsPlayable', 'true')
822 infos.update({'mediatype': entry['mediatype']})
823 if 'watched' in entry_keys:
824 infos.update({'playcount': (1, 0)[entry['watched']]})
825 if 'index' in entry_keys:
826 infos.update({'episode': entry['index']})
827 if 'episode' in entry_keys:
828 infos.update({'episode': entry['episode']})
829 if 'year' in entry_keys:
830 infos.update({'year': entry['year']})
831 if 'quality' in entry_keys:
832 quality = {'width': '960', 'height': '540'}
833 if entry['quality'] == '720':
834 quality = {'width': '1280', 'height': '720'}
835 if entry['quality'] == '1080':
836 quality = {'width': '1920', 'height': '1080'}
837 li.addStreamInfo('video', quality)
838 if 'tvshowtitle' in entry_keys:
839 infos.update({'tvshowtitle': entry['tvshowtitle']})
840 li.setInfo('video', infos)
843 def _generate_context_menu_items (self, entry, li):
844 """Adds context menue items to a Kodi list item
848 entry : :obj:`dict` of :obj:`str`
849 Entry that should be turned into a list item
851 li : :obj:`XMBC.ListItem`
852 Kodi list item instance
856 Kodi list item instance
860 entry_keys = entry.keys()
862 # action item templates
863 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
864 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
866 ['export_to_library', self.get_local_string(30018), 'export'],
867 ['remove_from_library', self.get_local_string(30030), 'remove'],
868 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
869 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
870 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
873 # build concrete action items
874 for action_item in actions:
875 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
877 # add or remove the movie/show/season/episode from & to the users "My List"
878 if 'in_my_list' in entry_keys:
879 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
880 elif 'queue' in entry_keys:
881 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
882 elif 'my_list' in entry_keys:
883 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
884 # rate the movie/show/season/episode on Netflix
885 items.append(action['rate_on_netflix'])
887 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
888 if 'type' in entry_keys:
890 if entry['type'] == 'movie':
891 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
892 items.append(action[action_type])
894 if entry['type'] == 'show' and 'title' in entry_keys:
895 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
896 items.append(action[action_type])
899 li.addContextMenuItems(items)
902 def log (self, msg, level=xbmc.LOGDEBUG):
903 """Adds a log entry to the Kodi log
908 Entry that should be turned into a list item
913 if isinstance(msg, unicode):
914 msg = msg.encode('utf-8')
915 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
917 def get_local_string (self, string_id):
918 """Returns the localized version of a string
922 string_id : :obj:`int`
923 ID of the string that shoudl be fetched
928 Requested string or empty string
930 src = xbmc if string_id < 30000 else self.get_addon()
931 locString = src.getLocalizedString(string_id)
932 if isinstance(locString, unicode):
933 locString = locString.encode('utf-8')
936 def get_inputstream_addon (self):
937 """Checks if the inputstream addon is installed & enabled.
938 Returns the type of the inputstream addon used or None if not found
943 Inputstream addon or None
945 type = 'inputstream.adaptive'
949 'method': 'Addons.GetAddonDetails',
952 'properties': ['enabled']
955 response = xbmc.executeJSONRPC(json.dumps(payload))
956 data = json.loads(response)
957 if not 'error' in data.keys():
958 if data['result']['addon']['enabled'] == True:
962 def set_library (self, library):
963 """Adds an instance of the Library class
967 library : :obj:`Library`
968 instance of the Library class
970 self.library = library
972 def track_event(self, event):
974 Send a tracking event if tracking is enabled
975 :param event: the string idetifier of the event
978 addon = self.get_addon()
979 # Check if tracking is enabled
980 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
982 #Get or Create Tracking id
983 tracking_id = addon.getSetting('tracking_id')
984 if tracking_id is '':
985 tracking_id = str(uuid4())
986 addon.setSetting('tracking_id', tracking_id)
987 # Send the tracking event
988 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
989 tracker.send('event', event)