2 # -*- coding: utf-8 -*-
4 # Created on: 13.01.2017
12 from os.path import join, isfile
13 from urllib import urlencode
14 from xbmcaddon import Addon
15 from uuid import uuid4
16 from utils import get_user_agent_for_current_platform
17 from UniversalAnalytics import Tracker
19 import cPickle as pickle
24 """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
26 def __init__ (self, plugin_handle=None, base_url=None):
27 """Fetches all needed info from Kodi & configures the baseline of the plugin
31 plugin_handle : :obj:`int`
37 addon = self.get_addon()
38 self.plugin_handle = plugin_handle
39 self.base_url = base_url
40 self.plugin = addon.getAddonInfo('name')
41 self.version = addon.getAddonInfo('version')
42 self.base_data_path = xbmc.translatePath(addon.getAddonInfo('profile'))
43 self.home_path = xbmc.translatePath('special://home')
44 self.plugin_path = addon.getAddonInfo('path')
45 self.cookie_path = self.base_data_path + 'COOKIE'
46 self.data_path = self.base_data_path + 'DATA'
47 self.config_path = join(self.base_data_path, 'config')
48 self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
49 self.verb_log = addon.getSetting('logging') == 'true'
50 self.default_fanart = addon.getAddonInfo('fanart')
55 """Returns a fresh addon instance"""
59 """Refresh the current list"""
60 return xbmc.executebuiltin('Container.Refresh')
62 def show_rating_dialog (self):
63 """Asks the user for a movie rating
68 Movie rating between 0 & 10
70 dlg = xbmcgui.Dialog()
71 return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
73 def show_search_term_dialog (self):
74 """Asks the user for a term to query the netflix search for
81 dlg = xbmcgui.Dialog()
82 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
87 def show_add_to_library_title_dialog (self, original_title):
88 """Asks the user for an alternative title for the show/movie that gets exported to the local library
92 original_title : :obj:`str`
93 Original title of the show (as suggested by the addon)
100 dlg = xbmcgui.Dialog()
101 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
103 def show_password_dialog (self):
104 """Asks the user for its Netflix password
111 dlg = xbmcgui.Dialog()
112 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
114 def show_email_dialog (self):
115 """Asks the user for its Netflix account email
120 Netflix account email
122 dlg = xbmcgui.Dialog()
123 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
125 def show_login_failed_notification (self):
126 """Shows notification that the login failed
133 dialog = xbmcgui.Dialog()
134 dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
137 def show_missing_inputstream_addon_notification (self):
138 """Shows notification that the inputstream addon couldn't be found
145 dialog = xbmcgui.Dialog()
146 dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
149 def show_no_search_results_notification (self):
150 """Shows notification that no search results could be found
157 dialog = xbmcgui.Dialog()
158 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
161 def show_no_seasons_notification (self):
162 """Shows notification that no seasons be found
169 dialog = xbmcgui.Dialog()
170 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
173 def set_setting (self, key, value):
174 """Public interface for the addons setSetting method
179 Setting could be set or not
181 return self.get_addon().setSetting(key, value)
183 def get_credentials (self):
184 """Returns the users stored credentials
188 :obj:`dict` of :obj:`str`
189 The users stored account data
192 'email': self.get_addon().getSetting('email'),
193 'password': self.get_addon().getSetting('password')
198 Returns the esn from settings
200 self.log(msg='Is FILE: ' + str(isfile(self.msl_data_path + 'msl_data.json')))
201 self.log(msg=self.get_addon().getSetting('esn'))
202 return self.get_addon().getSetting('esn')
204 def set_esn(self, esn):
206 Returns the esn from settings
208 stored_esn = self.get_esn()
209 if not stored_esn and esn:
210 self.set_setting('esn', esn)
211 self.delete_manifest_data()
215 def delete_manifest_data(self):
216 if isfile(self.msl_data_path + 'msl_data.json'):
217 remove(self.msl_data_path + 'msl_data.json')
218 if isfile(self.msl_data_path + 'manifest.json'):
219 remove(self.msl_data_path + 'manifest.json')
220 msl = MSL(kodi_helper=self)
221 msl.perform_key_handshake()
224 def get_dolby_setting(self):
226 Returns if the dolby sound is enabled
229 return self.get_addon().getSetting('enable_dolby_sound') == 'true'
231 def get_custom_library_settings (self):
232 """Returns the settings in regards to the custom library folder(s)
236 :obj:`dict` of :obj:`str`
237 The users library settings
240 'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
241 'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
244 def get_ssl_verification_setting (self):
245 """Returns the setting that describes if we should verify the ssl transport when loading data
252 return self.get_addon().getSetting('ssl_verification') == 'true'
254 def set_main_menu_selection (self, type):
255 """Persist the chosen main menu entry in memory
262 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
264 def get_main_menu_selection (self):
265 """Gets the persisted chosen main menu entry from memory
270 The last chosen main menu entry
272 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
274 def setup_memcache (self):
275 """Sets up the memory cache if not existant"""
276 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
277 # no cache setup yet, create one
278 if len(cached_items) < 1:
279 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
281 def invalidate_memcache (self):
282 """Invalidates the memory cache"""
283 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
285 def get_cached_item (self, cache_id):
286 """Returns an item from the in memory cache
290 cache_id : :obj:`str`
291 ID of the cache entry
296 Contents of the requested cache item or none
298 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
300 return cached_items.get(cache_id)
302 def add_cached_item (self, cache_id, contents):
303 """Adds an item to the in memory cache
307 cache_id : :obj:`str`
308 ID of the cache entry
313 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
314 cached_items.update({cache_id: contents})
315 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
317 def build_profiles_listing (self, profiles, action, build_url):
318 """Builds the profiles list Kodi screen
322 profiles : :obj:`list` of :obj:`dict` of :obj:`str`
323 List of user profiles
326 Action paramter to build the subsequent routes
328 build_url : :obj:`fn`
329 Function to build the subsequent routes
336 for profile in profiles:
337 url = build_url({'action': action, 'profile_id': profile['id']})
338 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
339 li.setProperty('fanart_image', self.default_fanart)
340 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
341 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
342 xbmcplugin.endOfDirectory(self.plugin_handle)
345 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
346 """Builds the video lists (my list, continue watching, etc.) Kodi screen
350 video_list_ids : :obj:`dict` of :obj:`str`
353 user_list_order : :obj:`list` of :obj:`str`
354 Ordered user lists, to determine what should be displayed in the main menue
356 actions : :obj:`dict` of :obj:`str`
357 Dictionary of actions to build subsequent routes
359 build_url : :obj:`fn`
360 Function to build the subsequent routes
368 for category in user_list_order:
369 for video_list_id in video_list_ids['user']:
370 if video_list_ids['user'][video_list_id]['name'] == category:
371 label = video_list_ids['user'][video_list_id]['displayName']
372 if category == 'netflixOriginals':
373 label = label.capitalize()
374 li = xbmcgui.ListItem(label=label)
375 li.setProperty('fanart_image', self.default_fanart)
376 # determine action route
377 action = actions['default']
378 if category in actions.keys():
379 action = actions[category]
380 # determine if the item should be selected
381 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
382 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
383 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
385 # add recommendations/genres as subfolders (save us some space on the home page)
387 'recommendations': self.get_local_string(30001),
388 'genres': self.get_local_string(30010)
390 for type in i18n_ids.keys():
391 # determine if the lists have contents
392 if len(video_list_ids[type]) > 0:
393 # determine action route
394 action = actions['default']
395 if type in actions.keys():
396 action = actions[type]
397 # determine if the item should be selected
398 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
399 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
400 li_rec.setProperty('fanart_image', self.default_fanart)
401 url_rec = build_url({'action': action, 'type': type})
402 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
404 # add search as subfolder
405 action = actions['default']
406 if 'search' in actions.keys():
407 action = actions[type]
408 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
409 li_rec.setProperty('fanart_image', self.default_fanart)
410 url_rec = build_url({'action': action, 'type': 'search'})
411 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
414 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
415 xbmcplugin.endOfDirectory(self.plugin_handle)
417 # (re)select the previously selected main menu entry
419 for item in preselect_items:
421 preselected_list_item = idx if item else None
422 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
423 if preselected_list_item != None:
424 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
427 def build_video_listing (self, video_list, actions, type, build_url):
428 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
432 video_list_ids : :obj:`dict` of :obj:`str`
435 actions : :obj:`dict` of :obj:`str`
436 Dictionary of actions to build subsequent routes
439 None or 'queue' f.e. when it´s a special video lists
441 build_url : :obj:`fn`
442 Function to build the subsequent routes
449 for video_list_id in video_list:
450 video = video_list[video_list_id]
451 li = xbmcgui.ListItem(label=video['title'])
452 # add some art to the item
453 li = self._generate_art_info(entry=video, li=li)
455 li, infos = self._generate_entry_info(entry=video, li=li)
456 li = self._generate_context_menu_items(entry=video, li=li)
457 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
458 if video_list[video_list_id]['type'] == 'movie':
459 # it´s a movie, so we need no subfolder & a route to play it
461 url = build_url({'action': 'play_video', 'video_id': video_list_id, 'infoLabels': infos})
463 # it´s a show, so we need a subfolder & route (for seasons)
465 params = {'action': actions[video['type']], 'show_id': video_list_id}
466 if 'tvshowtitle' in infos:
467 params['tvshowtitle'] = infos.get('tvshowtitle', '').encode('utf-8')
468 url = build_url(params)
469 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
471 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
472 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
473 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
474 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
475 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
476 xbmcplugin.endOfDirectory(self.plugin_handle)
479 def build_search_result_listing (self, video_list, actions, build_url):
480 """Builds the search results list Kodi screen
484 video_list : :obj:`dict` of :obj:`str`
485 List of videos or shows
487 actions : :obj:`dict` of :obj:`str`
488 Dictionary of actions to build subsequent routes
490 build_url : :obj:`fn`
491 Function to build the subsequent routes
498 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
500 def build_no_seasons_available (self):
501 """Builds the season list screen if no seasons could be found
508 self.show_no_seasons_notification()
509 xbmcplugin.endOfDirectory(self.plugin_handle)
512 def build_no_search_results_available (self, build_url, action):
513 """Builds the search results screen if no matches could be found
518 Action paramter to build the subsequent routes
520 build_url : :obj:`fn`
521 Function to build the subsequent routes
528 self.show_no_search_results_notification()
529 return xbmcplugin.endOfDirectory(self.plugin_handle)
531 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
532 """Builds the video lists screen for user subfolders (genres & recommendations)
536 video_list_ids : :obj:`dict` of :obj:`str`
540 List type (genre or recommendation)
543 Action paramter to build the subsequent routes
545 build_url : :obj:`fn`
546 Function to build the subsequent routes
553 for video_list_id in video_list_ids:
554 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
555 li.setProperty('fanart_image', self.default_fanart)
556 url = build_url({'action': action, 'video_list_id': video_list_id})
557 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
559 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
560 xbmcplugin.endOfDirectory(self.plugin_handle)
563 def build_season_listing (self, seasons_sorted, build_url):
564 """Builds the season list screen for a show
568 seasons_sorted : :obj:`list` of :obj:`dict` of :obj:`str`
569 Sorted list of season entries
571 build_url : :obj:`fn`
572 Function to build the subsequent routes
579 for season in seasons_sorted:
580 li = xbmcgui.ListItem(label=season['text'])
581 # add some art to the item
582 li = self._generate_art_info(entry=season, li=li)
584 li, infos = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
585 li = self._generate_context_menu_items(entry=season, li=li)
586 params = {'action': 'episode_list', 'season_id': season['id']}
587 if 'tvshowtitle' in infos:
588 params['tvshowtitle'] = infos.get('tvshowtitle', '').encode('utf-8')
589 url = build_url(params)
590 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
592 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
593 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
594 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
595 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
596 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
597 xbmcplugin.endOfDirectory(self.plugin_handle)
600 def build_episode_listing (self, episodes_sorted, build_url):
601 """Builds the episode list screen for a season of a show
605 episodes_sorted : :obj:`list` of :obj:`dict` of :obj:`str`
606 Sorted list of episode entries
608 build_url : :obj:`fn`
609 Function to build the subsequent routes
616 for episode in episodes_sorted:
617 li = xbmcgui.ListItem(label=episode['title'])
618 # add some art to the item
619 li = self._generate_art_info(entry=episode, li=li)
621 li, infos = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
622 li = self._generate_context_menu_items(entry=episode, li=li)
623 url = build_url({'action': 'play_video', 'video_id': episode['id'], 'start_offset': episode['bookmark'], 'infoLabels': infos})
624 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
626 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
627 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
628 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
629 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
630 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
631 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
632 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
633 xbmcplugin.endOfDirectory(self.plugin_handle)
636 def play_item (self, esn, video_id, start_offset=-1, infoLabels={}):
642 ESN needed for Widevine/Inputstream
644 video_id : :obj:`str`
645 ID of the video that should be played
647 start_offset : :obj:`str`
648 Offset to resume playback from (in seconds)
650 infoLabels : :obj:`str`
651 the listitem's infoLabels
659 addon = self.get_addon()
660 inputstream_addon = self.get_inputstream_addon()
661 if inputstream_addon == None:
662 self.show_missing_inputstream_addon_notification()
663 self.log(msg='Inputstream addon not found')
667 self.track_event('playVideo')
669 # check esn in settings
670 settings_esn = str(addon.getSetting('esn'))
671 if len(settings_esn) == 0:
672 addon.setSetting('esn', str(esn))
674 # inputstream addon properties
675 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
676 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
677 play_item.setContentLookup(False)
678 play_item.setMimeType('application/dash+xml')
679 play_item.setProperty(inputstream_addon + '.stream_headers', 'user-agent=' + get_user_agent_for_current_platform())
680 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
681 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
682 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
683 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=')
684 play_item.setProperty('inputstreamaddon', inputstream_addon)
686 # check if we have a bookmark e.g. start offset position
687 if int(start_offset) > 0:
688 play_item.setProperty('StartOffset', str(start_offset) + '.0')
690 if len(infoLabels) > 0:
691 play_item.setInfo('video', infoLabels)
692 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
694 def _generate_art_info (self, entry, li):
695 """Adds the art info from an entry to a Kodi list item
699 entry : :obj:`dict` of :obj:`str`
700 Entry that should be turned into a list item
702 li : :obj:`XMBC.ListItem`
703 Kodi list item instance
708 Kodi list item instance
710 art = {'fanart': self.default_fanart}
711 if 'boxarts' in dict(entry).keys():
713 'poster': entry['boxarts']['big'],
714 'landscape': entry['boxarts']['big'],
715 'thumb': entry['boxarts']['small'],
716 'fanart': entry['boxarts']['big']
718 if 'interesting_moment' in dict(entry).keys():
720 'poster': entry['interesting_moment'],
721 'fanart': entry['interesting_moment']
723 if 'thumb' in dict(entry).keys():
724 art.update({'thumb': entry['thumb']})
725 if 'fanart' in dict(entry).keys():
726 art.update({'fanart': entry['fanart']})
727 if 'poster' in dict(entry).keys():
728 art.update({'poster': entry['poster']})
732 def _generate_entry_info (self, entry, li, base_info={}):
733 """Adds the item info from an entry to a Kodi list item
737 entry : :obj:`dict` of :obj:`str`
738 Entry that should be turned into a list item
740 li : :obj:`XMBC.ListItem`
741 Kodi list item instance
743 base_info : :obj:`dict` of :obj:`str`
744 Additional info that overrules the entry info
749 Kodi list item instance
752 entry_keys = entry.keys()
753 if 'cast' in entry_keys and len(entry['cast']) > 0:
754 infos.update({'cast': entry['cast']})
755 if 'creators' in entry_keys and len(entry['creators']) > 0:
756 infos.update({'writer': entry['creators'][0]})
757 if 'directors' in entry_keys and len(entry['directors']) > 0:
758 infos.update({'director': entry['directors'][0]})
759 if 'genres' in entry_keys and len(entry['genres']) > 0:
760 infos.update({'genre': entry['genres'][0]})
761 if 'maturity' in entry_keys:
762 if 'mpaa' in entry_keys:
763 infos.update({'mpaa': entry['mpaa']})
765 if entry.get('maturity', None) is not None:
766 if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
767 infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
768 if 'rating' in entry_keys:
769 infos.update({'rating': int(entry['rating']) * 2})
770 if 'synopsis' in entry_keys:
771 infos.update({'plot': entry['synopsis']})
772 if 'plot' in entry_keys:
773 infos.update({'plot': entry['plot']})
774 if 'runtime' in entry_keys:
775 infos.update({'duration': entry['runtime']})
776 if 'duration' in entry_keys:
777 infos.update({'duration': entry['duration']})
778 if 'seasons_label' in entry_keys:
779 infos.update({'season': entry['seasons_label']})
780 if 'season' in entry_keys:
781 infos.update({'season': entry['season']})
782 if 'title' in entry_keys:
783 infos.update({'title': entry['title']})
784 if 'type' in entry_keys:
785 if entry['type'] == 'movie' or entry['type'] == 'episode':
786 li.setProperty('IsPlayable', 'true')
787 elif entry['type'] == 'show':
788 infos.update({'tvshowtitle': entry['title']})
789 if 'mediatype' in entry_keys:
790 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
791 li.setProperty('IsPlayable', 'true')
792 infos.update({'mediatype': entry['mediatype']})
793 if 'watched' in entry_keys:
794 infos.update({'playcount': (1, 0)[entry['watched']]})
795 if 'index' in entry_keys:
796 infos.update({'episode': entry['index']})
797 if 'episode' in entry_keys:
798 infos.update({'episode': entry['episode']})
799 if 'year' in entry_keys:
800 infos.update({'year': entry['year']})
801 if 'quality' in entry_keys:
802 quality = {'width': '960', 'height': '540'}
803 if entry['quality'] == '720':
804 quality = {'width': '1280', 'height': '720'}
805 if entry['quality'] == '1080':
806 quality = {'width': '1920', 'height': '1080'}
807 li.addStreamInfo('video', quality)
808 if 'tvshowtitle' in entry_keys:
809 infos.update({'tvshowtitle': entry.get('tvshowtitle', '').encode('utf-8')})
810 li.setInfo('video', infos)
813 def _generate_context_menu_items (self, entry, li):
814 """Adds context menue items to a Kodi list item
818 entry : :obj:`dict` of :obj:`str`
819 Entry that should be turned into a list item
821 li : :obj:`XMBC.ListItem`
822 Kodi list item instance
826 Kodi list item instance
830 entry_keys = entry.keys()
832 # action item templates
833 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
834 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
836 ['export_to_library', self.get_local_string(30018), 'export'],
837 ['remove_from_library', self.get_local_string(30030), 'remove'],
838 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
839 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
840 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
843 # build concrete action items
844 for action_item in actions:
845 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
847 # add or remove the movie/show/season/episode from & to the users "My List"
848 if 'in_my_list' in entry_keys:
849 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
850 elif 'queue' in entry_keys:
851 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
852 elif 'my_list' in entry_keys:
853 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
854 # rate the movie/show/season/episode on Netflix
855 items.append(action['rate_on_netflix'])
857 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
858 if 'type' in entry_keys:
860 if entry['type'] == 'movie':
861 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
862 items.append(action[action_type])
864 if entry['type'] == 'show' and 'title' in entry_keys:
865 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
866 items.append(action[action_type])
869 li.addContextMenuItems(items)
872 def log (self, msg, level=xbmc.LOGDEBUG):
873 """Adds a log entry to the Kodi log
878 Entry that should be turned into a list item
883 if isinstance(msg, unicode):
884 msg = msg.encode('utf-8')
885 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
887 def get_local_string (self, string_id):
888 """Returns the localized version of a string
892 string_id : :obj:`int`
893 ID of the string that shoudl be fetched
898 Requested string or empty string
900 src = xbmc if string_id < 30000 else self.get_addon()
901 locString = src.getLocalizedString(string_id)
902 if isinstance(locString, unicode):
903 locString = locString.encode('utf-8')
906 def get_inputstream_addon (self):
907 """Checks if the inputstream addon is installed & enabled.
908 Returns the type of the inputstream addon used or None if not found
913 Inputstream addon or None
915 type = 'inputstream.adaptive'
919 'method': 'Addons.GetAddonDetails',
922 'properties': ['enabled']
925 response = xbmc.executeJSONRPC(json.dumps(payload))
926 data = json.loads(response)
927 if not 'error' in data.keys():
928 if data['result']['addon']['enabled'] == True:
932 def set_library (self, library):
933 """Adds an instance of the Library class
937 library : :obj:`Library`
938 instance of the Library class
940 self.library = library
942 def track_event(self, event):
944 Send a tracking event if tracking is enabled
945 :param event: the string idetifier of the event
948 addon = self.get_addon()
949 # Check if tracking is enabled
950 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
952 #Get or Create Tracking id
953 tracking_id = addon.getSetting('tracking_id')
954 if tracking_id is '':
955 tracking_id = str(uuid4())
956 addon.setSetting('tracking_id', tracking_id)
957 # Send the tracking event
958 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
959 tracker.send('event', event)