2 # -*- coding: utf-8 -*-
4 # Created on: 13.01.2017
6 from urllib import urlencode, unquote
7 from urlparse import parse_qsl
8 from utils import noop, log
11 """Routes to the correct subfolder, dispatches actions & acts as a controller for the Kodi view & the Netflix model"""
13 def __init__ (self, netflix_session, kodi_helper, library, base_url, log_fn=noop):
14 """Takes the instances & configuration options needed to drive the plugin
18 netflix_session : :obj:`NetflixSession`
19 instance of the NetflixSession class
21 kodi_helper : :obj:`KodiHelper`
22 instance of the KodiHelper class
24 library : :obj:`Library`
25 instance of the Library class
33 self.netflix_session = netflix_session
34 self.kodi_helper = kodi_helper
35 self.library = library
36 self.base_url = base_url
40 def router (self, paramstring):
41 """Route to the requested subfolder & dispatch actions along the way
45 paramstring : :obj:`str`
48 params = self.parse_paramters(paramstring=paramstring)
51 if 'action' in params.keys() and params['action'] == 'logout':
52 return self.netflix_session.logout()
54 # check login & try to relogin if necessary
55 account = self.kodi_helper.get_credentials()
56 if account['email'] != '' and account['password'] != '':
57 if self.netflix_session.is_logged_in(account=account) != True:
58 if self.establish_session(account=account) != True:
59 return self.kodi_helper.show_login_failed_notification()
61 # check if we need to execute any actions before the actual routing
62 # gives back a dict of options routes might need
63 options = self.before_routing_action(params=params)
65 # check if one of the before routing options decided to killthe routing
68 if 'action' not in params.keys():
70 return self.show_profiles()
71 elif params['action'] == 'video_lists':
72 # list lists that contain other lists (starting point with recommendations, search, etc.)
73 return self.show_video_lists()
74 elif params['action'] == 'video_list':
75 # show a list of shows/movies
76 type = None if 'type' not in params.keys() else params['type']
77 return self.show_video_list(video_list_id=params['video_list_id'], type=type)
78 elif params['action'] == 'season_list':
79 # list of seasons for a show
80 return self.show_seasons(show_id=params['show_id'])
81 elif params['action'] == 'episode_list':
82 # list of episodes for a season
83 return self.show_episode_list(season_id=params['season_id'])
84 elif params['action'] == 'rating':
85 return self.rate_on_netflix(video_id=params['id'])
86 elif params['action'] == 'remove_from_list':
87 # removes a title from the users list on Netflix
88 self.kodi_helper.invalidate_memcache()
89 return self.remove_from_list(video_id=params['id'])
90 elif params['action'] == 'add_to_list':
91 # adds a title to the users list on Netflix
92 self.kodi_helper.invalidate_memcache()
93 return self.add_to_list(video_id=params['id'])
94 elif params['action'] == 'export':
95 # adds a title to the users list on Netflix
96 alt_title = self.kodi_helper.show_add_to_library_title_dialog(original_title=unquote(params['title']).decode('utf8'))
97 return self.export_to_library(video_id=params['id'], alt_title=alt_title)
98 elif params['action'] == 'remove':
99 # adds a title to the users list on Netflix
100 return self.remove_from_library(video_id=params['id'])
101 elif params['action'] == 'user-items' and params['type'] != 'search':
102 # display the lists (recommendations, genres, etc.)
103 return self.show_user_list(type=params['type'])
104 elif params['action'] == 'play_video':
105 self.play_video(video_id=params['video_id'], start_offset=params.get('start_offset', -1))
106 elif params['action'] == 'user-items' and params['type'] == 'search':
107 # if the user requested a search, ask for the term
108 term = self.kodi_helper.show_search_term_dialog()
109 return self.show_search_results(term=term)
111 raise ValueError('Invalid paramstring: {0}!'.format(paramstring))
115 def play_video (self, video_id, start_offset):
116 """Starts video playback
118 Note: This is just a dummy, inputstream is needed to play the vids
122 video_id : :obj:`str`
123 ID of the video that should be played
125 start_offset : :obj:`str`
126 Offset to resume playback from (in seconds)
129 esn = self.netflix_session.esn
130 return self.kodi_helper.play_item(esn=esn, video_id=video_id, start_offset=start_offset)
133 def show_search_results (self, term):
134 """Display a list of search results
144 If no results are available
146 has_search_results = False
147 search_results_raw = self.netflix_session.fetch_search_results(search_str=term)
148 # check for any errors
149 if self._is_dirty_response(response=search_results_raw):
152 # determine if we found something
153 if 'search' in search_results_raw['value']:
154 for key in search_results_raw['value']['search'].keys():
155 if self.netflix_session._is_size_key(key=key) == False:
156 has_search_results = search_results_raw['value']['search'][key]['titles']['length'] > 0
157 if has_search_results == False:
158 if search_results_raw['value']['search'][key].get('suggestions', False) != False:
159 for entry in search_results_raw['value']['search'][key]['suggestions']:
160 if self.netflix_session._is_size_key(key=entry) == False:
161 if search_results_raw['value']['search'][key]['suggestions'][entry]['relatedvideos']['length'] > 0:
162 has_search_results = True
165 # display that we haven't found a thing
166 if has_search_results == False:
167 return self.kodi_helper.build_no_search_results_available(build_url=self.build_url, action='search')
169 # list the search results
170 search_results = self.netflix_session.parse_search_results(response_data=search_results_raw)
171 # add more menaingful data to the search results
172 raw_search_contents = self.netflix_session.fetch_video_list_information(video_ids=search_results.keys())
173 # check for any errors
174 if self._is_dirty_response(response=raw_search_contents):
176 search_contents = self.netflix_session.parse_video_list(response_data=raw_search_contents)
177 actions = {'movie': 'play_video', 'show': 'season_list'}
178 return self.kodi_helper.build_search_result_listing(video_list=search_contents, actions=actions, build_url=self.build_url)
180 def show_user_list (self, type):
181 """List the users lists for shows/movies for recommendations/genres based on the given type
185 user_list_id : :obj:`str`
186 Type of list to display
188 video_list_ids_raw = self.netflix_session.fetch_video_list_ids()
189 # check for any errors
190 if self._is_dirty_response(response=video_list_ids_raw):
192 video_list_ids = self.netflix_session.parse_video_list_ids(response_data=video_list_ids_raw)
193 return self.kodi_helper.build_user_sub_listing(video_list_ids=video_list_ids[type], type=type, action='video_list', build_url=self.build_url)
195 def show_episode_list (self, season_id):
196 """Lists all episodes for a given season
200 season_id : :obj:`str`
201 ID of the season episodes should be displayed for
203 cache_id = 'episodes_' + season_id
204 if self.kodi_helper.has_cached_item(cache_id=cache_id):
205 episode_list = self.kodi_helper.get_cached_item(cache_id=cache_id)
207 raw_episode_list = self.netflix_session.fetch_episodes_by_season(season_id=season_id)
208 # check for any errors
209 if self._is_dirty_response(response=raw_episode_list):
211 # parse the raw Netflix data
212 episode_list = self.netflix_session.parse_episodes_by_season(response_data=raw_episode_list)
213 self.kodi_helper.add_cached_item(cache_id=cache_id, contents=episode_list)
215 # sort seasons by number (they´re coming back unsorted from the api)
217 for episode_id in episode_list:
218 episodes_sorted.append(int(episode_list[episode_id]['episode']))
219 episodes_sorted.sort()
222 return self.kodi_helper.build_episode_listing(episodes_sorted=episodes_sorted, episode_list=episode_list, build_url=self.build_url)
224 def show_seasons (self, show_id):
225 """Lists all seasons for a given show
230 ID of the show seasons should be displayed for
235 If no seasons are available
237 cache_id = 'season_' + show_id
238 if self.kodi_helper.has_cached_item(cache_id=cache_id):
239 season_list = self.kodi_helper.get_cached_item(cache_id=cache_id)
241 season_list_raw = self.netflix_session.fetch_seasons_for_show(id=show_id);
242 # check for any errors
243 if self._is_dirty_response(response=season_list_raw):
245 # check if we have sesons, announced shows that are not available yet have none
246 if 'seasons' not in season_list_raw['value']:
247 return self.kodi_helper.build_no_seasons_available()
248 # parse the seasons raw response from Netflix
249 season_list = self.netflix_session.parse_seasons(id=show_id, response_data=season_list_raw)
250 self.kodi_helper.add_cached_item(cache_id=cache_id, contents=season_list)
251 # sort seasons by index by default (they´re coming back unsorted from the api)
253 for season_id in season_list:
254 seasons_sorted.append(int(season_list[season_id]['idx']))
255 seasons_sorted.sort()
256 return self.kodi_helper.build_season_listing(seasons_sorted=seasons_sorted, season_list=season_list, build_url=self.build_url)
258 def show_video_list (self, video_list_id, type):
259 """List shows/movies based on the given video list id
263 video_list_id : :obj:`str`
264 ID of the video list that should be displayed
267 None or 'queue' f.e. when it´s a special video lists
269 if self.kodi_helper.has_cached_item(cache_id=video_list_id):
270 video_list = self.kodi_helper.get_cached_item(cache_id=video_list_id)
272 raw_video_list = self.netflix_session.fetch_video_list(list_id=video_list_id)
273 # check for any errors
274 if self._is_dirty_response(response=raw_video_list):
276 # parse the video list ids
277 if 'videos' in raw_video_list['value'].keys():
278 video_list = self.netflix_session.parse_video_list(response_data=raw_video_list)
279 self.kodi_helper.add_cached_item(cache_id=video_list_id, contents=video_list)
282 actions = {'movie': 'play_video', 'show': 'season_list'}
283 return self.kodi_helper.build_video_listing(video_list=video_list, actions=actions, type=type, build_url=self.build_url)
285 def show_video_lists (self):
286 """List the users video lists (recommendations, my list, etc.)"""
288 if self.kodi_helper.has_cached_item(cache_id=cache_id):
289 video_list_ids = self.kodi_helper.get_cached_item(cache_id=cache_id)
292 raw_video_list_ids = self.netflix_session.fetch_video_list_ids()
293 # check for any errors
294 if self._is_dirty_response(response=raw_video_list_ids):
296 # parse the video list ids
297 video_list_ids = self.netflix_session.parse_video_list_ids(response_data=raw_video_list_ids)
298 self.kodi_helper.add_cached_item(cache_id=cache_id, contents=video_list_ids)
299 # defines an order for the user list, as Netflix changes the order at every request
300 user_list_order = ['queue', 'continueWatching', 'topTen', 'netflixOriginals', 'trendingNow', 'newRelease', 'popularTitles']
301 # define where to route the user
302 actions = {'recommendations': 'user-items', 'genres': 'user-items', 'search': 'user-items', 'default': 'video_list'}
303 return self.kodi_helper.build_main_menu_listing(video_list_ids=video_list_ids, user_list_order=user_list_order, actions=actions, build_url=self.build_url)
306 def show_profiles (self):
307 """List the profiles for the active account"""
308 credentials = self.kodi_helper.get_credentials()
309 self.netflix_session.refresh_session_data(account=credentials)
310 profiles = self.netflix_session.profiles
311 return self.kodi_helper.build_profiles_listing(profiles=profiles, action='video_lists', build_url=self.build_url)
314 def rate_on_netflix (self, video_id):
315 """Rate a show/movie/season/episode on Netflix
319 video_list_id : :obj:`str`
320 ID of the video list that should be displayed
322 rating = self.kodi_helper.show_rating_dialog()
323 return self.netflix_session.rate_video(video_id=video_id, rating=rating)
326 def remove_from_list (self, video_id):
327 """Remove an item from 'My List' & refresh the view
331 video_list_id : :obj:`str`
332 ID of the video list that should be displayed
334 self.netflix_session.remove_from_list(video_id=video_id)
335 return self.kodi_helper.refresh()
338 def add_to_list (self, video_id):
339 """Add an item to 'My List' & refresh the view
343 video_list_id : :obj:`str`
344 ID of the video list that should be displayed
346 self.netflix_session.add_to_list(video_id=video_id)
347 return self.kodi_helper.refresh()
350 def export_to_library (self, video_id, alt_title):
351 """Adds an item to the local library
355 video_id : :obj:`str`
356 ID of the movie or show
358 alt_title : :obj:`str`
359 Alternative title (for the folder written to disc)
361 metadata = self.netflix_session.fetch_metadata(id=video_id)
362 # check for any errors
363 if self._is_dirty_response(response=metadata):
365 video = metadata['video']
367 if video['type'] == 'movie':
368 self.library.add_movie(title=video['title'], alt_title=alt_title, year=video['year'], video_id=video_id, build_url=self.build_url)
369 if video['type'] == 'show':
371 for season in video['seasons']:
372 for episode in season['episodes']:
373 episodes.append({'season': season['seq'], 'episode': episode['seq'], 'id': episode['id']})
375 self.library.add_show(title=video['title'], alt_title=alt_title, episodes=episodes, build_url=self.build_url)
376 return self.kodi_helper.refresh()
379 def remove_from_library (self, video_id, season=None, episode=None):
380 """Removes an item from the local library
384 video_id : :obj:`str`
385 ID of the movie or show
387 metadata = self.netflix_session.fetch_metadata(id=video_id)
388 # check for any errors
389 if self._is_dirty_response(response=metadata):
391 video = metadata['video']
393 if video['type'] == 'movie':
394 self.library.remove_movie(title=video['title'], year=video['year'])
395 if video['type'] == 'show':
396 self.library.remove_show(title=video['title'])
397 return self.kodi_helper.refresh()
400 def establish_session(self, account):
401 """Checks if we have an cookie with an active sessions, otherwise tries to login the user
405 account : :obj:`dict` of :obj:`str`
406 Dict containing an email & a password property
411 If we don't have an active session & the user couldn't be logged in
413 return True if self.netflix_session.is_logged_in(account=account) else self.netflix_session.login(account=account)
416 def before_routing_action (self, params):
417 """Executes actions before the actual routing takes place:
419 - Check if account data has been stored, if not, asks for it
420 - Check if the profile should be changed (and changes if so)
421 - Establishes a session if no action route is given
425 params : :obj:`dict` of :obj:`str`
430 :obj:`dict` of :obj:`str`
431 Options that can be provided by this hook & used later in the routing process
434 credentials = self.kodi_helper.get_credentials()
435 # check if we have user settings, if not, set em
436 if credentials['email'] == '':
437 email = self.kodi_helper.show_email_dialog()
438 self.kodi_helper.set_setting(key='email', value=email)
439 credentials['email'] = email
440 if credentials['password'] == '':
441 password = self.kodi_helper.show_password_dialog()
442 self.kodi_helper.set_setting(key='password', value=password)
443 credentials['password'] = password
444 # persist & load main menu selection
446 self.kodi_helper.set_main_menu_selection(type=params['type'])
447 options['main_menu_selection'] = self.kodi_helper.get_main_menu_selection()
448 # check and switch the profile if needed
449 if self.check_for_designated_profile_change(params=params):
450 self.kodi_helper.invalidate_memcache()
451 self.netflix_session.switch_profile(profile_id=params['profile_id'], account=credentials)
452 # check login, in case of main menu
453 if 'action' not in params:
454 self.establish_session(account=credentials)
457 def check_for_designated_profile_change (self, params):
458 """Checks if the profile needs to be switched
462 params : :obj:`dict` of :obj:`str`
468 Profile should be switched or not
470 # check if we need to switch the user
471 if 'guid' not in self.netflix_session.user_data:
473 current_profile_id = self.netflix_session.user_data['guid']
474 return 'profile_id' in params and current_profile_id != params['profile_id']
476 def parse_paramters (self, paramstring):
477 """Tiny helper to convert a url paramstring into a dictionary
481 paramstring : :obj:`str`
482 Url query params (in url string notation)
486 :obj:`dict` of :obj:`str`
487 Url query params (as a dictionary)
489 return dict(parse_qsl(paramstring))
491 def _is_expired_session (self, response):
492 """Checks if a response error is based on an invalid session
496 response : :obj:`dict` of :obj:`str`
497 Error response object
502 Error is based on an invalid session
504 return 'error' in response and 'code' in response and str(response['code']) == '401'
506 def _is_dirty_response (self, response):
507 """Checks if a response contains an error & if the error is based on an invalid session, it tries a relogin
511 response : :obj:`dict` of :obj:`str`
512 Success response object or Error response object
517 Response contains error or not
519 # check for any errors
520 if 'error' in response:
521 # check if we do not have a valid session, in case that happens: (re)login
522 if self._is_expired_session(response=response):
523 if self.establish_session(account=self.kodi_helper.get_credentials()):
525 message = response['message'] if 'message' in response else ''
526 code = response['code'] if 'code' in response else ''
527 self.log(msg='[ERROR]: ' + message + '::' + str(code))
531 def build_url(self, query):
532 """Tiny helper to transform a dict into a url + querystring
536 query : :obj:`dict` of :obj:`str`
537 List of paramters to be url encoded
542 Url + querystring based on the param
544 return self.base_url + '?' + urlencode(query)