2 # -*- coding: utf-8 -*-
4 # Created on: 13.01.2017
10 from os.path import join
11 from urllib import urlencode
12 from xbmcaddon import Addon
13 from uuid import uuid4
14 from UniversalAnalytics import Tracker
16 import cPickle as pickle
21 """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
23 def __init__ (self, plugin_handle=None, base_url=None):
24 """Fetches all needed info from Kodi & configures the baseline of the plugin
28 plugin_handle : :obj:`int`
34 self.plugin_handle = plugin_handle
35 self.base_url = base_url
37 self.plugin = self.addon.getAddonInfo('name')
38 self.base_data_path = xbmc.translatePath(self.addon.getAddonInfo('profile'))
39 self.home_path = xbmc.translatePath('special://home')
40 self.plugin_path = self.addon.getAddonInfo('path')
41 self.cookie_path = self.base_data_path + 'COOKIE'
42 self.data_path = self.base_data_path + 'DATA'
43 self.config_path = join(self.base_data_path, 'config')
44 self.msl_data_path = xbmc.translatePath('special://profile/addon_data/service.msl').decode('utf-8') + '/'
45 self.verb_log = self.addon.getSetting('logging') == 'true'
46 self.default_fanart = self.addon.getAddonInfo('fanart')
51 """Refresh the current list"""
52 return xbmc.executebuiltin('Container.Refresh')
54 def show_rating_dialog (self):
55 """Asks the user for a movie rating
60 Movie rating between 0 & 10
62 dlg = xbmcgui.Dialog()
63 return dlg.numeric(heading=self.get_local_string(string_id=30019) + ' ' + self.get_local_string(string_id=30022), type=0)
65 def show_search_term_dialog (self):
66 """Asks the user for a term to query the netflix search for
73 dlg = xbmcgui.Dialog()
74 term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
79 def show_add_to_library_title_dialog (self, original_title):
80 """Asks the user for an alternative title for the show/movie that gets exported to the local library
84 original_title : :obj:`str`
85 Original title of the show (as suggested by the addon)
92 dlg = xbmcgui.Dialog()
93 return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
95 def show_password_dialog (self):
96 """Asks the user for its Netflix password
103 dlg = xbmcgui.Dialog()
104 return dlg.input(self.get_local_string(string_id=30004), type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
106 def show_email_dialog (self):
107 """Asks the user for its Netflix account email
112 Netflix account email
114 dlg = xbmcgui.Dialog()
115 return dlg.input(self.get_local_string(string_id=30005), type=xbmcgui.INPUT_ALPHANUM)
117 def show_login_failed_notification (self):
118 """Shows notification that the login failed
125 dialog = xbmcgui.Dialog()
126 dialog.notification(self.get_local_string(string_id=30008), self.get_local_string(string_id=30009), xbmcgui.NOTIFICATION_ERROR, 5000)
129 def show_missing_inputstream_addon_notification (self):
130 """Shows notification that the inputstream addon couldn't be found
137 dialog = xbmcgui.Dialog()
138 dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
141 def show_no_search_results_notification (self):
142 """Shows notification that no search results could be found
149 dialog = xbmcgui.Dialog()
150 dialog.notification(self.get_local_string(string_id=30011), self.get_local_string(string_id=30013))
153 def show_no_seasons_notification (self):
154 """Shows notification that no seasons be found
161 dialog = xbmcgui.Dialog()
162 dialog.notification(self.get_local_string(string_id=30010), self.get_local_string(string_id=30012))
165 def set_setting (self, key, value):
166 """Public interface for the addons setSetting method
171 Setting could be set or not
173 return self.addon.setSetting(key, value)
175 def get_credentials (self):
176 """Returns the users stored credentials
180 :obj:`dict` of :obj:`str`
181 The users stored account data
184 'email': self.addon.getSetting('email'),
185 'password': self.addon.getSetting('password')
188 def get_dolby_setting(self):
190 Returns if the dolby sound is enabled
193 return self.addon.getSetting('enable_dolby_sound') == 'true'
195 def get_custom_library_settings (self):
196 """Returns the settings in regards to the custom library folder(s)
200 :obj:`dict` of :obj:`str`
201 The users library settings
204 'enablelibraryfolder': self.addon.getSetting('enablelibraryfolder'),
205 'customlibraryfolder': self.addon.getSetting('customlibraryfolder')
208 def get_ssl_verification_setting (self):
209 """Returns the setting that describes if we should verify the ssl transport when loading data
216 return self.addon.getSetting('ssl_verification') == 'true'
218 def set_main_menu_selection (self, type):
219 """Persist the chosen main menu entry in memory
226 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('main_menu_selection', type)
228 def get_main_menu_selection (self):
229 """Gets the persisted chosen main menu entry from memory
234 The last chosen main menu entry
236 return xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('main_menu_selection')
238 def setup_memcache (self):
239 """Sets up the memory cache if not existant"""
240 cached_items = xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache')
241 # no cache setup yet, create one
242 if len(cached_items) < 1:
243 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
245 def invalidate_memcache (self):
246 """Invalidates the memory cache"""
247 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps({}))
249 def has_cached_item (self, cache_id):
250 """Checks if the requested item is in memory cache
254 cache_id : :obj:`str`
255 ID of the cache entry
262 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
263 return cache_id in cached_items.keys()
265 def get_cached_item (self, cache_id):
266 """Returns an item from the in memory cache
270 cache_id : :obj:`str`
271 ID of the cache entry
276 Contents of the requested cache item or none
278 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
279 if self.has_cached_item(cache_id) != True:
281 return cached_items[cache_id]
283 def add_cached_item (self, cache_id, contents):
284 """Adds an item to the in memory cache
288 cache_id : :obj:`str`
289 ID of the cache entry
294 cached_items = pickle.loads(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getProperty('memcache'))
295 cached_items.update({cache_id: contents})
296 xbmcgui.Window(xbmcgui.getCurrentWindowId()).setProperty('memcache', pickle.dumps(cached_items))
298 def build_profiles_listing (self, profiles, action, build_url):
299 """Builds the profiles list Kodi screen
303 profiles : :obj:`dict` of :obj:`str`
304 List of user profiles
307 Action paramter to build the subsequent routes
309 build_url : :obj:`fn`
310 Function to build the subsequent routes
317 for profile_id in profiles:
318 profile = profiles[profile_id]
319 url = build_url({'action': action, 'profile_id': profile_id})
320 li = xbmcgui.ListItem(label=profile['profileName'], iconImage=profile['avatar'])
321 li.setProperty('fanart_image', self.default_fanart)
322 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
323 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
324 xbmcplugin.endOfDirectory(self.plugin_handle)
327 def build_main_menu_listing (self, video_list_ids, user_list_order, actions, build_url):
328 """Builds the video lists (my list, continue watching, etc.) Kodi screen
332 video_list_ids : :obj:`dict` of :obj:`str`
335 user_list_order : :obj:`list` of :obj:`str`
336 Ordered user lists, to determine what should be displayed in the main menue
338 actions : :obj:`dict` of :obj:`str`
339 Dictionary of actions to build subsequent routes
341 build_url : :obj:`fn`
342 Function to build the subsequent routes
350 for category in user_list_order:
351 for video_list_id in video_list_ids['user']:
352 if video_list_ids['user'][video_list_id]['name'] == category:
353 label = video_list_ids['user'][video_list_id]['displayName']
354 if category == 'netflixOriginals':
355 label = label.capitalize()
356 li = xbmcgui.ListItem(label=label)
357 li.setProperty('fanart_image', self.default_fanart)
358 # determine action route
359 action = actions['default']
360 if category in actions.keys():
361 action = actions[category]
362 # determine if the item should be selected
363 preselect_items.append((False, True)[category == self.get_main_menu_selection()])
364 url = build_url({'action': action, 'video_list_id': video_list_id, 'type': category})
365 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
367 # add recommendations/genres as subfolders (save us some space on the home page)
369 'recommendations': self.get_local_string(30001),
370 'genres': self.get_local_string(30010)
372 for type in i18n_ids.keys():
373 # determine if the lists have contents
374 if len(video_list_ids[type]) > 0:
375 # determine action route
376 action = actions['default']
377 if type in actions.keys():
378 action = actions[type]
379 # determine if the item should be selected
380 preselect_items.append((False, True)[type == self.get_main_menu_selection()])
381 li_rec = xbmcgui.ListItem(label=i18n_ids[type])
382 li_rec.setProperty('fanart_image', self.default_fanart)
383 url_rec = build_url({'action': action, 'type': type})
384 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
386 # add search as subfolder
387 action = actions['default']
388 if 'search' in actions.keys():
389 action = actions[type]
390 li_rec = xbmcgui.ListItem(label=self.get_local_string(30011))
391 li_rec.setProperty('fanart_image', self.default_fanart)
392 url_rec = build_url({'action': action, 'type': 'search'})
393 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url_rec, listitem=li_rec, isFolder=True)
396 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
397 xbmcplugin.endOfDirectory(self.plugin_handle)
399 # (re)select the previously selected main menu entry
401 for item in preselect_items:
403 preselected_list_item = idx if item else None
404 preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
405 if preselected_list_item != None:
406 xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(xbmcgui.Window(xbmcgui.getCurrentWindowId()).getFocusId()), str(preselected_list_item)))
409 def build_video_listing (self, video_list, actions, type, build_url):
410 """Builds the video lists (my list, continue watching, etc.) contents Kodi screen
414 video_list_ids : :obj:`dict` of :obj:`str`
417 actions : :obj:`dict` of :obj:`str`
418 Dictionary of actions to build subsequent routes
421 None or 'queue' f.e. when it´s a special video lists
423 build_url : :obj:`fn`
424 Function to build the subsequent routes
431 for video_list_id in video_list:
432 video = video_list[video_list_id]
433 if type != 'queue' or (type == 'queue' and video['in_my_list'] == True):
434 li = xbmcgui.ListItem(label=video['title'])
435 # add some art to the item
436 li = self._generate_art_info(entry=video, li=li)
437 # it´s a show, so we need a subfolder & route (for seasons)
439 url = build_url({'action': actions[video['type']], 'show_id': video_list_id})
440 # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away
441 if video_list[video_list_id]['type'] == 'movie':
442 # it´s a movie, so we need no subfolder & a route to play it
444 url = build_url({'action': 'play_video', 'video_id': video_list_id})
446 li = self._generate_entry_info(entry=video, li=li)
447 li = self._generate_context_menu_items(entry=video, li=li)
448 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder)
450 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
451 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
452 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
453 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
454 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
455 xbmcplugin.endOfDirectory(self.plugin_handle)
458 def build_search_result_listing (self, video_list, actions, build_url):
459 """Builds the search results list Kodi screen
463 video_list : :obj:`dict` of :obj:`str`
464 List of videos or shows
466 actions : :obj:`dict` of :obj:`str`
467 Dictionary of actions to build subsequent routes
469 build_url : :obj:`fn`
470 Function to build the subsequent routes
477 return self.build_video_listing(video_list=video_list, actions=actions, type='search', build_url=build_url)
479 def build_no_seasons_available (self):
480 """Builds the season list screen if no seasons could be found
487 self.show_no_seasons_notification()
488 xbmcplugin.endOfDirectory(self.plugin_handle)
491 def build_no_search_results_available (self, build_url, action):
492 """Builds the search results screen if no matches could be found
497 Action paramter to build the subsequent routes
499 build_url : :obj:`fn`
500 Function to build the subsequent routes
507 self.show_no_search_results_notification()
508 return xbmcplugin.endOfDirectory(self.plugin_handle)
510 def build_user_sub_listing (self, video_list_ids, type, action, build_url):
511 """Builds the video lists screen for user subfolders (genres & recommendations)
515 video_list_ids : :obj:`dict` of :obj:`str`
519 List type (genre or recommendation)
522 Action paramter to build the subsequent routes
524 build_url : :obj:`fn`
525 Function to build the subsequent routes
532 for video_list_id in video_list_ids:
533 li = xbmcgui.ListItem(video_list_ids[video_list_id]['displayName'])
534 li.setProperty('fanart_image', self.default_fanart)
535 url = build_url({'action': action, 'video_list_id': video_list_id})
536 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
538 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
539 xbmcplugin.endOfDirectory(self.plugin_handle)
542 def build_season_listing (self, seasons_sorted, season_list, build_url):
543 """Builds the season list screen for a show
547 seasons_sorted : :obj:`list` of :obj:`str`
548 Sorted season indexes
550 season_list : :obj:`dict` of :obj:`str`
551 List of season entries
553 build_url : :obj:`fn`
554 Function to build the subsequent routes
561 for index in seasons_sorted:
562 for season_id in season_list:
563 season = season_list[season_id]
564 if int(season['idx']) == index:
565 li = xbmcgui.ListItem(label=season['text'])
566 # add some art to the item
567 li = self._generate_art_info(entry=season, li=li)
569 li = self._generate_entry_info(entry=season, li=li, base_info={'mediatype': 'season'})
570 li = self._generate_context_menu_items(entry=season, li=li)
571 url = build_url({'action': 'episode_list', 'season_id': season_id})
572 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
574 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
575 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
576 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
577 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
578 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
579 xbmcplugin.endOfDirectory(self.plugin_handle)
582 def build_episode_listing (self, episodes_sorted, episode_list, build_url):
583 """Builds the episode list screen for a season of a show
587 episodes_sorted : :obj:`list` of :obj:`str`
588 Sorted episode indexes
590 episode_list : :obj:`dict` of :obj:`str`
591 List of episode entries
593 build_url : :obj:`fn`
594 Function to build the subsequent routes
601 for index in episodes_sorted:
602 for episode_id in episode_list:
603 episode = episode_list[episode_id]
604 if int(episode['episode']) == index:
605 li = xbmcgui.ListItem(label=episode['title'])
606 # add some art to the item
607 li = self._generate_art_info(entry=episode, li=li)
609 li = self._generate_entry_info(entry=episode, li=li, base_info={'mediatype': 'episode'})
610 li = self._generate_context_menu_items(entry=episode, li=li)
611 url = build_url({'action': 'play_video', 'video_id': episode_id, 'start_offset': episode['bookmark']})
612 xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
614 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
615 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
616 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
617 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
618 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
619 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
620 xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
621 xbmcplugin.endOfDirectory(self.plugin_handle)
624 def play_item (self, esn, video_id, start_offset=-1):
630 ESN needed for Widevine/Inputstream
632 video_id : :obj:`str`
633 ID of the video that should be played
635 start_offset : :obj:`str`
636 Offset to resume playback from (in seconds)
643 inputstream_addon = self.get_inputstream_addon()
644 if inputstream_addon == None:
645 self.show_missing_inputstream_addon_notification()
646 self.log(msg='Inputstream addon not found')
650 self.track_event('playVideo')
652 # check esn in settings
653 settings_esn = str(self.addon.getSetting('esn'))
654 if len(settings_esn) == 0:
655 self.addon.setSetting('esn', str(esn))
657 # inputstream addon properties
658 msl_service_url = 'http://localhost:' + str(self.addon.getSetting('msl_service_port'))
659 play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
660 play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
661 play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
662 play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
663 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=')
664 play_item.setProperty('inputstreamaddon', inputstream_addon)
666 # check if we have a bookmark e.g. start offset position
667 if int(start_offset) > 0:
668 play_item.setProperty('StartOffset', str(start_offset) + '.0')
669 return xbmcplugin.setResolvedUrl(self.plugin_handle, True, listitem=play_item)
671 def _generate_art_info (self, entry, li):
672 """Adds the art info from an entry to a Kodi list item
676 entry : :obj:`dict` of :obj:`str`
677 Entry that should be turned into a list item
679 li : :obj:`XMBC.ListItem`
680 Kodi list item instance
685 Kodi list item instance
687 art = {'fanart': self.default_fanart}
688 if 'boxarts' in dict(entry).keys():
690 'poster': entry['boxarts']['big'],
691 'landscape': entry['boxarts']['big'],
692 'thumb': entry['boxarts']['small'],
693 'fanart': entry['boxarts']['big']
695 if 'interesting_moment' in dict(entry).keys():
697 'poster': entry['interesting_moment'],
698 'fanart': entry['interesting_moment']
700 if 'thumb' in dict(entry).keys():
701 art.update({'thumb': entry['thumb']})
702 if 'fanart' in dict(entry).keys():
703 art.update({'fanart': entry['fanart']})
704 if 'poster' in dict(entry).keys():
705 art.update({'poster': entry['poster']})
709 def _generate_entry_info (self, entry, li, base_info={}):
710 """Adds the item info from an entry to a Kodi list item
714 entry : :obj:`dict` of :obj:`str`
715 Entry that should be turned into a list item
717 li : :obj:`XMBC.ListItem`
718 Kodi list item instance
720 base_info : :obj:`dict` of :obj:`str`
721 Additional info that overrules the entry info
726 Kodi list item instance
729 entry_keys = entry.keys()
730 if 'cast' in entry_keys and len(entry['cast']) > 0:
731 infos.update({'cast': entry['cast']})
732 if 'creators' in entry_keys and len(entry['creators']) > 0:
733 infos.update({'writer': entry['creators'][0]})
734 if 'directors' in entry_keys and len(entry['directors']) > 0:
735 infos.update({'director': entry['directors'][0]})
736 if 'genres' in entry_keys and len(entry['genres']) > 0:
737 infos.update({'genre': entry['genres'][0]})
738 if 'maturity' in entry_keys:
739 if 'mpaa' in entry_keys:
740 infos.update({'mpaa': entry['mpaa']})
742 infos.update({'mpaa': str(entry['maturity']['board']) + '-' + str(entry['maturity']['value'])})
743 if 'rating' in entry_keys:
744 infos.update({'rating': int(entry['rating']) * 2})
745 if 'synopsis' in entry_keys:
746 infos.update({'plot': entry['synopsis']})
747 if 'plot' in entry_keys:
748 infos.update({'plot': entry['plot']})
749 if 'runtime' in entry_keys:
750 infos.update({'duration': entry['runtime']})
751 if 'duration' in entry_keys:
752 infos.update({'duration': entry['duration']})
753 if 'seasons_label' in entry_keys:
754 infos.update({'season': entry['seasons_label']})
755 if 'season' in entry_keys:
756 infos.update({'season': entry['season']})
757 if 'title' in entry_keys:
758 infos.update({'title': entry['title']})
759 if 'type' in entry_keys:
760 if entry['type'] == 'movie' or entry['type'] == 'episode':
761 li.setProperty('IsPlayable', 'true')
762 if 'mediatype' in entry_keys:
763 if entry['mediatype'] == 'movie' or entry['mediatype'] == 'episode':
764 li.setProperty('IsPlayable', 'true')
765 infos.update({'mediatype': entry['mediatype']})
766 if 'watched' in entry_keys:
767 infos.update({'playcount': (1, 0)[entry['watched']]})
768 if 'index' in entry_keys:
769 infos.update({'episode': entry['index']})
770 if 'episode' in entry_keys:
771 infos.update({'episode': entry['episode']})
772 if 'year' in entry_keys:
773 infos.update({'year': entry['year']})
774 if 'quality' in entry_keys:
775 quality = {'width': '960', 'height': '540'}
776 if entry['quality'] == '720':
777 quality = {'width': '1280', 'height': '720'}
778 if entry['quality'] == '1080':
779 quality = {'width': '1920', 'height': '1080'}
780 li.addStreamInfo('video', quality)
781 li.setInfo('video', infos)
784 def _generate_context_menu_items (self, entry, li):
785 """Adds context menue items to a Kodi list item
789 entry : :obj:`dict` of :obj:`str`
790 Entry that should be turned into a list item
792 li : :obj:`XMBC.ListItem`
793 Kodi list item instance
797 Kodi list item instance
801 entry_keys = entry.keys()
803 # action item templates
804 encoded_title = urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
805 url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
807 ['export_to_library', self.get_local_string(30018), 'export'],
808 ['remove_from_library', self.get_local_string(30030), 'remove'],
809 ['rate_on_netflix', self.get_local_string(30019), 'rating'],
810 ['remove_from_my_list', self.get_local_string(30020), 'remove_from_list'],
811 ['add_to_my_list', self.get_local_string(30021), 'add_to_list']
814 # build concrete action items
815 for action_item in actions:
816 action.update({action_item[0]: [action_item[1], url_tmpl.replace('%action%', action_item[2])]})
818 # add or remove the movie/show/season/episode from & to the users "My List"
819 if 'in_my_list' in entry_keys:
820 items.append(action['remove_from_my_list']) if entry['in_my_list'] else items.append(action['add_to_my_list'])
821 elif 'queue' in entry_keys:
822 items.append(action['remove_from_my_list']) if entry['queue'] else items.append(action['add_to_my_list'])
823 elif 'my_list' in entry_keys:
824 items.append(action['remove_from_my_list']) if entry['my_list'] else items.append(action['add_to_my_list'])
825 # rate the movie/show/season/episode on Netflix
826 items.append(action['rate_on_netflix'])
828 # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
829 if 'type' in entry_keys:
831 if entry['type'] == 'movie':
832 action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
833 items.append(action[action_type])
835 if entry['type'] == 'show' and 'title' in entry_keys:
836 action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
837 items.append(action[action_type])
840 li.addContextMenuItems(items)
843 def log (self, msg, level=xbmc.LOGDEBUG):
844 """Adds a log entry to the Kodi log
849 Entry that should be turned into a list item
854 if isinstance(msg, unicode):
855 msg = msg.encode('utf-8')
856 xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
858 def get_local_string (self, string_id):
859 """Returns the localized version of a string
863 string_id : :obj:`int`
864 ID of the string that shoudl be fetched
869 Requested string or empty string
871 src = xbmc if string_id < 30000 else self.addon
872 locString = src.getLocalizedString(string_id)
873 if isinstance(locString, unicode):
874 locString = locString.encode('utf-8')
877 def get_inputstream_addon (self):
878 """Checks if the inputstream addon is installed & enabled.
879 Returns the type of the inputstream addon used or None if not found
884 Inputstream addon or None
886 type = 'inputstream.adaptive'
890 'method': 'Addons.GetAddonDetails',
893 'properties': ['enabled']
896 response = xbmc.executeJSONRPC(json.dumps(payload))
897 data = json.loads(response)
898 if not 'error' in data.keys():
899 if data['result']['addon']['enabled'] == True:
903 def set_library (self, library):
904 """Adds an instance of the Library class
908 library : :obj:`Library`
909 instance of the Library class
911 self.library = library
913 def track_event(self, event):
915 Send a tracking event if tracking is enabled
916 :param event: the string idetifier of the event
919 # Check if tracking is enabled
920 enable_tracking = (self.addon.getSetting('enable_tracking') == 'true')
922 #Get or Create Tracking id
923 tracking_id = self.addon.getSetting('tracking_id')
924 if tracking_id is '':
925 tracking_id = str(uuid4())
926 self.addon.setSetting('tracking_id', tracking_id)
927 # Send the tracking event
928 tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
929 tracker.send('event', event)