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 self.log(msg='Is FILE: ' + str(isfile(self.msl_data_path + 'msl_data.json')))
200 self.log(msg=self.get_addon().getSetting('esn'))
201 return self.get_addon().getSetting('esn')
203 def set_esn(self, esn):
205 Returns the esn from settings
207 stored_esn = self.get_esn()
208 hidden_esn = self.get_addon().getSetting('hidden_esn')
209 if hidden_esn == '' and esn:
210 self.set_setting('esn', esn)
211 self.set_setting('hidden_esn', esn)
212 self.delete_manifest_data()
215 if hidden_esn != stored_esn:
216 self.set_setting('hidden_esn', stored_esn)
217 self.delete_manifest_data()
220 if not stored_esn and esn:
221 self.set_setting('esn', esn)
222 self.delete_manifest_data()
226 def delete_manifest_data(self):
227 if isfile(self.msl_data_path + 'msl_data.json'):
228 remove(self.msl_data_path + 'msl_data.json')
229 if isfile(self.msl_data_path + 'manifest.json'):
230 remove(self.msl_data_path + 'manifest.json')
231 msl = MSL(kodi_helper=self)
232 msl.perform_key_handshake()
235 def get_dolby_setting(self):
237 Returns if the dolby sound is enabled
240 return self.get_addon().getSetting('enable_dolby_sound') == 'true'
242 def get_custom_library_settings (self):
243 """Returns the settings in regards to the custom library folder(s)
247 :obj:`dict` of :obj:`str`
248 The users library settings
251 'enablelibraryfolder': self.get_addon().getSetting('enablelibraryfolder'),
252 'customlibraryfolder': self.get_addon().getSetting('customlibraryfolder')
255 def get_ssl_verification_setting (self):
256 """Returns the setting that describes if we should verify the ssl transport when loading data
263 return self.get_addon().getSetting('ssl_verification') == 'true'
265 def set_main_menu_selection (self, type):
266 """Persist the chosen main menu entry in memory
273 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
275 def get_main_menu_selection (self):
276 """Gets the persisted chosen main menu entry from memory
281 The last chosen main menu entry
283 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
285 def setup_memcache (self):
286 """Sets up the memory cache if not existant"""
287 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
288 # no cache setup yet, create one
289 if len(cached_items) < 1:
290 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
292 def invalidate_memcache (self):
293 """Invalidates the memory cache"""
294 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
296 def has_cached_item (self, cache_id):
297 """Checks if the requested item is in memory cache
301 cache_id : :obj:`str`
302 ID of the cache entry
309 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
310 return cache_id in cached_items.keys()
312 def get_cached_item (self, cache_id):
313 """Returns an item from the in memory cache
317 cache_id : :obj:`str`
318 ID of the cache entry
323 Contents of the requested cache item or none
325 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
326 if self.has_cached_item(cache_id) != True:
328 return cached_items[cache_id]
330 def add_cached_item (self, cache_id, contents):
331 """Adds an item to the in memory cache
335 cache_id : :obj:`str`
336 ID of the cache entry
341 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
342 cached_items.update({cache_id: contents})
343 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
345 def build_profiles_listing (self, profiles, action, build_url):
346 """Builds the profiles list Kodi screen
350 profiles : :obj:`dict` of :obj:`str`
351 List of user profiles
354 Action paramter to build the subsequent routes
356 build_url : :obj:`fn`
357 Function to build the subsequent routes
364 for profile_id in profiles:
365 profile = profiles[profile_id]
366 url = build_url({'action': action, 'profile_id': profile_id})
367 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
368 li.setProperty('fanart_image', self.default_fanart)
369 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
370 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
371 xbmcplugin.endOfDirectory(self.plugin_handle)
374 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
375 """Builds the video lists (my list, continue watching, etc.) Kodi screen
379 video_list_ids : :obj:`dict` of :obj:`str`
382 user_list_order : :obj:`list` of :obj:`str`
383 Ordered user lists, to determine what should be displayed in the main menue
385 actions : :obj:`dict` of :obj:`str`
386 Dictionary of actions to build subsequent routes
388 build_url : :obj:`fn`
389 Function to build the subsequent routes
397 for category in user_list_order:
398 for video_list_id in video_list_ids['user']:
399 if video_list_ids['user'][video_list_id]['name'] == category:
400 label = video_list_ids['user'][video_list_id]['displayName']
401 if category == 'netflixOriginals':
402 label = label.capitalize()
403 li = xbmcgui.ListItem(label=label)
404 li.setProperty('fanart_image', self.default_fanart)
405 # determine action route
406 action = actions['default']
407 if category in actions.keys():
408 action = actions[category]
409 # determine if the item should be selected
410 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
411 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
412 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
414 # add recommendations/genres as subfolders (save us some space on the home page)
416 'recommendations': self.get_local_string(30001),
417 'genres': self.get_local_string(30010)
419 for type in i18n_ids.keys():
420 # determine if the lists have contents
421 if len(video_list_ids[type]) > 0:
422 # determine action route
423 action = actions['default']
424 if type in actions.keys():
425 action = actions[type]
426 # determine if the item should be selected
427 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
428 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
429 li_rec.setProperty('fanart_image', self.default_fanart)
430 url_rec = build_url({'action': action, 'type': type})
431 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
433 # add search as subfolder
434 action = actions['default']
435 if 'search' in actions.keys():
436 action = actions[type]
437 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
438 li_rec.setProperty('fanart_image', self.default_fanart)
439 url_rec = build_url({'action': action, 'type': 'search'})
440 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
443 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
444 xbmcplugin.endOfDirectory(self.plugin_handle)
446 # (re)select the previously selected main menu entry
448 for item in preselect_items:
450 preselected_list_item = idx if item else None
451 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
452 if preselected_list_item != None:
453 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
456 def build_video_listing (self, video_list, actions, type, build_url):
457 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
461 video_list_ids : :obj:`dict` of :obj:`str`
464 actions : :obj:`dict` of :obj:`str`
465 Dictionary of actions to build subsequent routes
468 None or 'queue' f.e. when it´s a special video lists
470 build_url : :obj:`fn`
471 Function to build the subsequent routes
478 for video_list_id in video_list:
479 video = video_list[video_list_id]
480 li = xbmcgui.ListItem(label=video['title'])
481 # add some art to the item
482 li = self._generate_art_info(entry=video, li=li)
483 # it´s a show, so we need a subfolder & route (for seasons)
485 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
486 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
487 if video_list[video_list_id]['type'] == 'movie':
488 # it´s a movie, so we need no subfolder & a route to play it
490 url = build_url({'action': 'play_video', 'video_id': video_list_id})
492 li = self._generate_entry_info(entry=video, li=li)
493 li = self._generate_context_menu_items(entry=video, li=li)
494 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
496 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
497 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
498 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
499 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
500 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
501 xbmcplugin.endOfDirectory(self.plugin_handle)
504 def build_search_result_listing (self, video_list, actions, build_url):
505 """Builds the search results list Kodi screen
509 video_list : :obj:`dict` of :obj:`str`
510 List of videos or shows
512 actions : :obj:`dict` of :obj:`str`
513 Dictionary of actions to build subsequent routes
515 build_url : :obj:`fn`
516 Function to build the subsequent routes
523 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
525 def build_no_seasons_available (self):
526 """Builds the season list screen if no seasons could be found
533 self.show_no_seasons_notification()
534 xbmcplugin.endOfDirectory(self.plugin_handle)
537 def build_no_search_results_available (self, build_url, action):
538 """Builds the search results screen if no matches could be found
543 Action paramter to build the subsequent routes
545 build_url : :obj:`fn`
546 Function to build the subsequent routes
553 self.show_no_search_results_notification()
554 return xbmcplugin.endOfDirectory(self.plugin_handle)
556 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
557 """Builds the video lists screen for user subfolders (genres & recommendations)
561 video_list_ids : :obj:`dict` of :obj:`str`
565 List type (genre or recommendation)
568 Action paramter to build the subsequent routes
570 build_url : :obj:`fn`
571 Function to build the subsequent routes
578 for video_list_id in video_list_ids:
579 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
580 li.setProperty('fanart_image', self.default_fanart)
581 url = build_url({'action': action, 'video_list_id': video_list_id})
582 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
584 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
585 xbmcplugin.endOfDirectory(self.plugin_handle)
588 def build_season_listing (self, seasons_sorted, season_list, build_url):
589 """Builds the season list screen for a show
593 seasons_sorted : :obj:`list` of :obj:`str`
594 Sorted season indexes
596 season_list : :obj:`dict` of :obj:`str`
597 List of season entries
599 build_url : :obj:`fn`
600 Function to build the subsequent routes
607 for index in seasons_sorted:
608 for season_id in season_list:
609 season = season_list[season_id]
610 if int(season['idx']) == index:
611 li = xbmcgui.ListItem(label=season['text'])
612 # add some art to the item
613 li = self._generate_art_info(entry=season, li=li)
615 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
616 li = self._generate_context_menu_items(entry=season, li=li)
617 url = build_url({'action': 'episode_list', 'season_id': season_id})
618 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
620 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
621 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
622 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
623 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
624 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
625 xbmcplugin.endOfDirectory(self.plugin_handle)
628 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
629 """Builds the episode list screen for a season of a show
633 episodes_sorted : :obj:`list` of :obj:`str`
634 Sorted episode indexes
636 episode_list : :obj:`dict` of :obj:`str`
637 List of episode entries
639 build_url : :obj:`fn`
640 Function to build the subsequent routes
647 for index in episodes_sorted:
648 for episode_id in episode_list:
649 episode = episode_list[episode_id]
650 if int(episode['episode']) == index:
651 li = xbmcgui.ListItem(label=episode['title'])
652 # add some art to the item
653 li = self._generate_art_info(entry=episode, li=li)
655 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
656 li = self._generate_context_menu_items(entry=episode, li=li)
657 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
658 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
660 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
661 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
662 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
663 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
664 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
665 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
666 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
667 xbmcplugin.endOfDirectory(self.plugin_handle)
670 def play_item (self, esn, video_id, start_offset=-1):
676 ESN needed for Widevine/Inputstream
678 video_id : :obj:`str`
679 ID of the video that should be played
681 start_offset : :obj:`str`
682 Offset to resume playback from (in seconds)
690 addon = self.get_addon()
691 inputstream_addon = self.get_inputstream_addon()
692 if inputstream_addon == None:
693 self.show_missing_inputstream_addon_notification()
694 self.log(msg='Inputstream addon not found')
698 self.track_event('playVideo')
700 # check esn in settings
701 settings_esn = str(addon.getSetting('esn'))
702 if len(settings_esn) == 0:
703 addon.setSetting('esn', str(esn))
705 # inputstream addon properties
706 msl_service_url = 'http://localhost:' + str(addon.getSetting('msl_service_port'))
707 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
708 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
709 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
710 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
711 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=')
712 play_item.setProperty('inputstreamaddon', inputstream_addon)
714 # check if we have a bookmark e.g. start offset position
715 if int(start_offset) > 0:
716 play_item.setProperty('StartOffset', str(start_offset) + '.0')
717 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
719 def _generate_art_info (self, entry, li):
720 """Adds the art info from an entry to a Kodi list item
724 entry : :obj:`dict` of :obj:`str`
725 Entry that should be turned into a list item
727 li : :obj:`XMBC.ListItem`
728 Kodi list item instance
733 Kodi list item instance
735 art = {'fanart': self.default_fanart}
736 if 'boxarts' in dict(entry).keys():
738 'poster': entry['boxarts']['big'],
739 'landscape': entry['boxarts']['big'],
740 'thumb': entry['boxarts']['small'],
741 'fanart': entry['boxarts']['big']
743 if 'interesting_moment' in dict(entry).keys():
745 'poster': entry['interesting_moment'],
746 'fanart': entry['interesting_moment']
748 if 'thumb' in dict(entry).keys():
749 art.update({'thumb': entry['thumb']})
750 if 'fanart' in dict(entry).keys():
751 art.update({'fanart': entry['fanart']})
752 if 'poster' in dict(entry).keys():
753 art.update({'poster': entry['poster']})
757 def _generate_entry_info (self, entry, li, base_info={}):
758 """Adds the item info from an entry to a Kodi list item
762 entry : :obj:`dict` of :obj:`str`
763 Entry that should be turned into a list item
765 li : :obj:`XMBC.ListItem`
766 Kodi list item instance
768 base_info : :obj:`dict` of :obj:`str`
769 Additional info that overrules the entry info
774 Kodi list item instance
777 entry_keys = entry.keys()
778 if 'cast' in entry_keys and len(entry['cast']) > 0:
779 infos.update({'cast': entry['cast']})
780 if 'creators' in entry_keys and len(entry['creators']) > 0:
781 infos.update({'writer': entry['creators'][0]})
782 if 'directors' in entry_keys and len(entry['directors']) > 0:
783 infos.update({'director': entry['directors'][0]})
784 if 'genres' in entry_keys and len(entry['genres']) > 0:
785 infos.update({'genre': entry['genres'][0]})
786 if 'maturity' in entry_keys:
787 if 'mpaa' in entry_keys:
788 infos.update({'mpaa': entry['mpaa']})
790 if entry.get('maturity', None) is not None:
791 if entry['maturity']['board'] is not None and entry['maturity']['value'] is not None:
792 infos.update({'mpaa': str(entry['maturity']['board'].encode('utf-8')) + '-' + str(entry['maturity']['value'].encode('utf-8'))})
793 if 'rating' in entry_keys:
794 infos.update({'rating': int(entry['rating']) * 2})
795 if 'synopsis' in entry_keys:
796 infos.update({'plot': entry['synopsis']})
797 if 'plot' in entry_keys:
798 infos.update({'plot': entry['plot']})
799 if 'runtime' in entry_keys:
800 infos.update({'duration': entry['runtime']})
801 if 'duration' in entry_keys:
802 infos.update({'duration': entry['duration']})
803 if 'seasons_label' in entry_keys:
804 infos.update({'season': entry['seasons_label']})
805 if 'season' in entry_keys:
806 infos.update({'season': entry['season']})
807 if 'title' in entry_keys:
808 infos.update({'title': entry['title']})
809 if 'type' in entry_keys:
810 if entry['type'] == 'movie' or entry['type'] == 'episode':
811 li.setProperty('IsPlayable', 'true')
812 if 'mediatype' in entry_keys:
813 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
814 li.setProperty('IsPlayable', 'true')
815 infos.update({'mediatype': entry['mediatype']})
816 if 'watched' in entry_keys:
817 infos.update({'playcount': (1, 0)[entry['watched']]})
818 if 'index' in entry_keys:
819 infos.update({'episode': entry['index']})
820 if 'episode' in entry_keys:
821 infos.update({'episode': entry['episode']})
822 if 'year' in entry_keys:
823 infos.update({'year': entry['year']})
824 if 'quality' in entry_keys:
825 quality = {'width': '960', 'height': '540'}
826 if entry['quality'] == '720':
827 quality = {'width': '1280', 'height': '720'}
828 if entry['quality'] == '1080':
829 quality = {'width': '1920', 'height': '1080'}
830 li.addStreamInfo('video', quality)
831 li.setInfo('video', infos)
834 def _generate_context_menu_items (self, entry, li):
835 """Adds context menue items to a Kodi list item
839 entry : :obj:`dict` of :obj:`str`
840 Entry that should be turned into a list item
842 li : :obj:`XMBC.ListItem`
843 Kodi list item instance
847 Kodi list item instance
851 entry_keys = entry.keys()
853 # action item templates
854 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
855 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
857 ['export_to_library', self.get_local_string(30018), 'export'],
858 ['remove_from_library', self.get_local_string(30030), 'remove'],
859 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
860 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
861 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
864 # build concrete action items
865 for action_item in actions:
866 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
868 # add or remove the movie/show/season/episode from & to the users "My List"
869 if 'in_my_list' in entry_keys:
870 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
871 elif 'queue' in entry_keys:
872 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
873 elif 'my_list' in entry_keys:
874 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
875 # rate the movie/show/season/episode on Netflix
876 items.append(action['rate_on_netflix'])
878 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
879 if 'type' in entry_keys:
881 if entry['type'] == 'movie':
882 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
883 items.append(action[action_type])
885 if entry['type'] == 'show' and 'title' in entry_keys:
886 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
887 items.append(action[action_type])
890 li.addContextMenuItems(items)
893 def log (self, msg, level=xbmc.LOGDEBUG):
894 """Adds a log entry to the Kodi log
899 Entry that should be turned into a list item
904 if isinstance(msg, unicode):
905 msg = msg.encode('utf-8')
906 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
908 def get_local_string (self, string_id):
909 """Returns the localized version of a string
913 string_id : :obj:`int`
914 ID of the string that shoudl be fetched
919 Requested string or empty string
921 src = xbmc if string_id < 30000 else self.get_addon()
922 locString = src.getLocalizedString(string_id)
923 if isinstance(locString, unicode):
924 locString = locString.encode('utf-8')
927 def get_inputstream_addon (self):
928 """Checks if the inputstream addon is installed & enabled.
929 Returns the type of the inputstream addon used or None if not found
934 Inputstream addon or None
936 type = 'inputstream.adaptive'
940 'method': 'Addons.GetAddonDetails',
943 'properties': ['enabled']
946 response = xbmc.executeJSONRPC(json.dumps(payload))
947 data = json.loads(response)
948 if not 'error' in data.keys():
949 if data['result']['addon']['enabled'] == True:
953 def set_library (self, library):
954 """Adds an instance of the Library class
958 library : :obj:`Library`
959 instance of the Library class
961 self.library = library
963 def track_event(self, event):
965 Send a tracking event if tracking is enabled
966 :param event: the string idetifier of the event
969 addon = self.get_addon()
970 # Check if tracking is enabled
971 enable_tracking = (addon.getSetting('enable_tracking') == 'true')
973 #Get or Create Tracking id
974 tracking_id = addon.getSetting('tracking_id')
975 if tracking_id is '':
976 tracking_id = str(uuid4())
977 addon.setSetting('tracking_id', tracking_id)
978 # Send the tracking event
979 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
980 tracker.send('event', event)