From: Sebastian Golasch Date: Thu, 9 Mar 2017 13:53:05 +0000 (+0100) Subject: Merge pull request #15 from asciidisco/feat/netflix-service X-Git-Url: http://git.code-monkey.de/?p=plugin.video.netflix.git;a=commitdiff_plain;h=2b1c5fa796929d522c9b9c366d8e01423b15121c;hp=86455dfff7e4878e5192dd97991f2e96a3021739 Merge pull request #15 from asciidisco/feat/netflix-service Feat/netflix service --- diff --git a/addon.py b/addon.py index 5b2d7e8..4549bf9 100644 --- a/addon.py +++ b/addon.py @@ -4,7 +4,6 @@ # Created on: 13.01.2017 import sys -from resources.lib.NetflixSession import NetflixSession from resources.lib.KodiHelper import KodiHelper from resources.lib.Navigation import Navigation from resources.lib.Library import Library @@ -18,19 +17,12 @@ kodi_helper = KodiHelper( plugin_handle=plugin_handle, base_url=base_url ) -netflix_session = NetflixSession( - cookie_path=kodi_helper.cookie_path, - data_path=kodi_helper.data_path, - verify_ssl=kodi_helper.get_ssl_verification_setting(), - log_fn=kodi_helper.log -) library = Library( root_folder=kodi_helper.base_data_path, library_settings=kodi_helper.get_custom_library_settings(), log_fn=kodi_helper.log ) navigation = Navigation( - netflix_session=netflix_session, kodi_helper=kodi_helper, library=library, base_url=base_url, diff --git a/addon.xml b/addon.xml index 920b7c1..973551c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/resources/language/English/strings.po b/resources/language/English/strings.po index cfa6cd0..37448b0 100644 --- a/resources/language/English/strings.po +++ b/resources/language/English/strings.po @@ -1,7 +1,7 @@ # Kodi Media Center language file # Addon Name: Netflix # Addon id: plugin.video.netflix -# Addon version: 0.9.11 +# Addon version: 0.10.6 # Addon Provider: libdev + jojo + asciidisco msgid "" msgstr "" @@ -156,3 +156,7 @@ msgstr "" msgctxt "#30034" msgid "ESN (set automatically, can be changed manually)" msgstr "" + +msgctxt "#30035" +msgid "Inputstream Addon Settings..." +msgstr "" diff --git a/resources/language/German/strings.po b/resources/language/German/strings.po index 494840c..8f6e5da 100644 --- a/resources/language/German/strings.po +++ b/resources/language/German/strings.po @@ -1,7 +1,7 @@ # Kodi Media Center language file # Addon Name: Netflix # Addon id: plugin.video.netflix -# Addon version: 0.9.11 +# Addon version: 0.10.6 # Addon Provider: libdev + jojo + asciidisco msgid "" msgstr "" @@ -156,3 +156,7 @@ msgstr "Benutze Dolby Ton" msgctxt "#30034" msgid "ESN (set automatically, can be changed manually)" msgstr "ESN (änderbar, wird auto. gesetzt)" + +msgctxt "#30035" +msgid "Inputstream Addon Settings..." +msgstr "Inputstream Addon Settings..." diff --git a/resources/lib/KodiHelper.py b/resources/lib/KodiHelper.py index 7386ece..7519c99 100644 --- a/resources/lib/KodiHelper.py +++ b/resources/lib/KodiHelper.py @@ -20,7 +20,7 @@ except: class KodiHelper: """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos""" - def __init__ (self, plugin_handle, base_url): + def __init__ (self, plugin_handle=None, base_url=None): """Fetches all needed info from Kodi & configures the baseline of the plugin Parameters @@ -430,22 +430,21 @@ class KodiHelper: """ for video_list_id in video_list: video = video_list[video_list_id] - if type != 'queue' or (type == 'queue' and video['in_my_list'] == True): - li = xbmcgui.ListItem(label=video['title']) - # add some art to the item - li = self._generate_art_info(entry=video, li=li) - # it´s a show, so we need a subfolder & route (for seasons) - isFolder = True - url = build_url({'action': actions[video['type']], 'show_id': video_list_id}) - # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away - if video_list[video_list_id]['type'] == 'movie': - # it´s a movie, so we need no subfolder & a route to play it - isFolder = False - url = build_url({'action': 'play_video', 'video_id': video_list_id}) - # add list item info - li = self._generate_entry_info(entry=video, li=li) - li = self._generate_context_menu_items(entry=video, li=li) - xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder) + li = xbmcgui.ListItem(label=video['title']) + # add some art to the item + li = self._generate_art_info(entry=video, li=li) + # it´s a show, so we need a subfolder & route (for seasons) + isFolder = True + url = build_url({'action': actions[video['type']], 'show_id': video_list_id}) + # lists can be mixed with shows & movies, therefor we need to check if its a movie, so play it right away + if video_list[video_list_id]['type'] == 'movie': + # it´s a movie, so we need no subfolder & a route to play it + isFolder = False + url = build_url({'action': 'play_video', 'video_id': video_list_id}) + # add list item info + li = self._generate_entry_info(entry=video, li=li) + li = self._generate_context_menu_items(entry=video, li=li) + xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=isFolder) xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL) xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE) diff --git a/resources/lib/MSL.py b/resources/lib/MSL.py index db9d68b..83a06ee 100644 --- a/resources/lib/MSL.py +++ b/resources/lib/MSL.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Module: MSL +# Created on: 26.01.2017 + import base64 import gzip import json diff --git a/resources/lib/MSLHttpRequestHandler.py b/resources/lib/MSLHttpRequestHandler.py index 037d8c4..610b3d4 100644 --- a/resources/lib/MSLHttpRequestHandler.py +++ b/resources/lib/MSLHttpRequestHandler.py @@ -1,13 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Module: MSLHttpRequestHandler +# Created on: 26.01.2017 + import BaseHTTPServer import base64 from urlparse import urlparse, parse_qs from MSL import MSL from KodiHelper import KodiHelper -kodi_helper = KodiHelper( - plugin_handle=None, - base_url=None -) +kodi_helper = KodiHelper() msl = MSL(kodi_helper) diff --git a/resources/lib/Navigation.py b/resources/lib/Navigation.py index d4d3f7f..37fa6ec 100644 --- a/resources/lib/Navigation.py +++ b/resources/lib/Navigation.py @@ -3,21 +3,21 @@ # Module: Navigation # Created on: 13.01.2017 -from urllib import urlencode, unquote +import urllib +import urllib2 +import json +from xbmcaddon import Addon from urlparse import parse_qsl from utils import noop, log class Navigation: """Routes to the correct subfolder, dispatches actions & acts as a controller for the Kodi view & the Netflix model""" - def __init__ (self, netflix_session, kodi_helper, library, base_url, log_fn=noop): + def __init__ (self, kodi_helper, library, base_url, log_fn=noop): """Takes the instances & configuration options needed to drive the plugin Parameters ---------- - netflix_session : :obj:`NetflixSession` - instance of the NetflixSession class - kodi_helper : :obj:`KodiHelper` instance of the KodiHelper class @@ -30,7 +30,6 @@ class Navigation: log_fn : :obj:`fn` optional log function """ - self.netflix_session = netflix_session self.kodi_helper = kodi_helper self.library = library self.base_url = base_url @@ -47,14 +46,18 @@ class Navigation: """ params = self.parse_paramters(paramstring=paramstring) + # open foreign settings dialog + if 'mode' in params.keys() and params['mode'] == 'openSettings': + return self.open_settings(params['url']) + # log out the user if 'action' in params.keys() and params['action'] == 'logout': - return self.netflix_session.logout() + return self.call_netflix_service({'method': 'logout'}) # check login & try to relogin if necessary account = self.kodi_helper.get_credentials() if account['email'] != '' and account['password'] != '': - if self.netflix_session.is_logged_in(account=account) != True: + if self.call_netflix_service({'method': 'is_logged_in'}) != True: if self.establish_session(account=account) != True: return self.kodi_helper.show_login_failed_notification() @@ -93,7 +96,7 @@ class Navigation: return self.add_to_list(video_id=params['id']) elif params['action'] == 'export': # adds a title to the users list on Netflix - alt_title = self.kodi_helper.show_add_to_library_title_dialog(original_title=unquote(params['title']).decode('utf8')) + alt_title = self.kodi_helper.show_add_to_library_title_dialog(original_title=urllib.unquote(params['title']).decode('utf8')) return self.export_to_library(video_id=params['id'], alt_title=alt_title) elif params['action'] == 'remove': # adds a title to the users list on Netflix @@ -125,8 +128,7 @@ class Navigation: start_offset : :obj:`str` Offset to resume playback from (in seconds) """ - # widevine esn - esn = self.netflix_session.esn + esn = self.call_netflix_service({'method': 'get_esn'}) return self.kodi_helper.play_item(esn=esn, video_id=video_id, start_offset=start_offset) @log @@ -143,37 +145,10 @@ class Navigation: bool If no results are available """ - has_search_results = False - search_results_raw = self.netflix_session.fetch_search_results(search_str=term) - # check for any errors - if self._is_dirty_response(response=search_results_raw): - return False - - # determine if we found something - if 'search' in search_results_raw['value']: - for key in search_results_raw['value']['search'].keys(): - if self.netflix_session._is_size_key(key=key) == False: - has_search_results = search_results_raw['value']['search'][key]['titles']['length'] > 0 - if has_search_results == False: - if search_results_raw['value']['search'][key].get('suggestions', False) != False: - for entry in search_results_raw['value']['search'][key]['suggestions']: - if self.netflix_session._is_size_key(key=entry) == False: - if search_results_raw['value']['search'][key]['suggestions'][entry]['relatedvideos']['length'] > 0: - has_search_results = True - - - # display that we haven't found a thing - if has_search_results == False: - return self.kodi_helper.build_no_search_results_available(build_url=self.build_url, action='search') - - # list the search results - search_results = self.netflix_session.parse_search_results(response_data=search_results_raw) - # add more menaingful data to the search results - raw_search_contents = self.netflix_session.fetch_video_list_information(video_ids=search_results.keys()) + search_contents = self.call_netflix_service({'method': 'search', 'term': term}) # check for any errors - if self._is_dirty_response(response=raw_search_contents): + if self._is_dirty_response(response=search_contents): return False - search_contents = self.netflix_session.parse_video_list(response_data=raw_search_contents) actions = {'movie': 'play_video', 'show': 'season_list'} return self.kodi_helper.build_search_result_listing(video_list=search_contents, actions=actions, build_url=self.build_url) @@ -185,11 +160,10 @@ class Navigation: user_list_id : :obj:`str` Type of list to display """ - video_list_ids_raw = self.netflix_session.fetch_video_list_ids() + video_list_ids = self.call_netflix_service({'method': 'fetch_video_list_ids', 'type': type}) # check for any errors - if self._is_dirty_response(response=video_list_ids_raw): + if self._is_dirty_response(response=video_list_ids): return False - video_list_ids = self.netflix_session.parse_video_list_ids(response_data=video_list_ids_raw) 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) def show_episode_list (self, season_id): @@ -204,12 +178,11 @@ class Navigation: if self.kodi_helper.has_cached_item(cache_id=cache_id): episode_list = self.kodi_helper.get_cached_item(cache_id=cache_id) else: - raw_episode_list = self.netflix_session.fetch_episodes_by_season(season_id=season_id) + episode_list = self.call_netflix_service({'method': 'fetch_episodes_by_season', 'season_id': season_id}) # check for any errors - if self._is_dirty_response(response=raw_episode_list): + if self._is_dirty_response(response=episode_list): return False # parse the raw Netflix data - episode_list = self.netflix_session.parse_episodes_by_season(response_data=raw_episode_list) self.kodi_helper.add_cached_item(cache_id=cache_id, contents=episode_list) # sort seasons by number (they´re coming back unsorted from the api) @@ -238,15 +211,14 @@ class Navigation: if self.kodi_helper.has_cached_item(cache_id=cache_id): season_list = self.kodi_helper.get_cached_item(cache_id=cache_id) else: - season_list_raw = self.netflix_session.fetch_seasons_for_show(id=show_id); + season_list = self.call_netflix_service({'method': 'fetch_seasons_for_show', 'show_id': show_id}) # check for any errors - if self._is_dirty_response(response=season_list_raw): + if self._is_dirty_response(response=season_list): return False # check if we have sesons, announced shows that are not available yet have none - if 'seasons' not in season_list_raw['value']: + if len(season_list) == 0: return self.kodi_helper.build_no_seasons_available() # parse the seasons raw response from Netflix - season_list = self.netflix_session.parse_seasons(id=show_id, response_data=season_list_raw) self.kodi_helper.add_cached_item(cache_id=cache_id, contents=season_list) # sort seasons by index by default (they´re coming back unsorted from the api) seasons_sorted = [] @@ -269,16 +241,13 @@ class Navigation: if self.kodi_helper.has_cached_item(cache_id=video_list_id): video_list = self.kodi_helper.get_cached_item(cache_id=video_list_id) else: - raw_video_list = self.netflix_session.fetch_video_list(list_id=video_list_id) + video_list = self.call_netflix_service({'method': 'fetch_video_list', 'list_id': video_list_id}) # check for any errors - if self._is_dirty_response(response=raw_video_list): + if self._is_dirty_response(response=video_list): return False # parse the video list ids - if 'videos' in raw_video_list['value'].keys(): - video_list = self.netflix_session.parse_video_list(response_data=raw_video_list) + if len(video_list) > 0: self.kodi_helper.add_cached_item(cache_id=video_list_id, contents=video_list) - else: - video_list = [] actions = {'movie': 'play_video', 'show': 'season_list'} return self.kodi_helper.build_video_listing(video_list=video_list, actions=actions, type=type, build_url=self.build_url) @@ -289,12 +258,11 @@ class Navigation: video_list_ids = self.kodi_helper.get_cached_item(cache_id=cache_id) else: # fetch video lists - raw_video_list_ids = self.netflix_session.fetch_video_list_ids() + video_list_ids = self.call_netflix_service({'method': 'fetch_video_list_ids'}) # check for any errors - if self._is_dirty_response(response=raw_video_list_ids): + if self._is_dirty_response(response=video_list_ids): return False # parse the video list ids - video_list_ids = self.netflix_session.parse_video_list_ids(response_data=raw_video_list_ids) self.kodi_helper.add_cached_item(cache_id=cache_id, contents=video_list_ids) # defines an order for the user list, as Netflix changes the order at every request user_list_order = ['queue', 'continueWatching', 'topTen', 'netflixOriginals', 'trendingNow', 'newRelease', 'popularTitles'] @@ -305,9 +273,9 @@ class Navigation: @log def show_profiles (self): """List the profiles for the active account""" - credentials = self.kodi_helper.get_credentials() - self.netflix_session.refresh_session_data(account=credentials) - profiles = self.netflix_session.profiles + profiles = self.call_netflix_service({'method': 'list_profiles'}) + if len(profiles) == 0: + return self.kodi_helper.show_login_failed_notification() return self.kodi_helper.build_profiles_listing(profiles=profiles, action='video_lists', build_url=self.build_url) @log @@ -320,7 +288,7 @@ class Navigation: ID of the video list that should be displayed """ rating = self.kodi_helper.show_rating_dialog() - return self.netflix_session.rate_video(video_id=video_id, rating=rating) + return self.call_netflix_service({'method': 'rate_video', 'video_id': video_id, 'rating': rating}) @log def remove_from_list (self, video_id): @@ -331,7 +299,7 @@ class Navigation: video_list_id : :obj:`str` ID of the video list that should be displayed """ - self.netflix_session.remove_from_list(video_id=video_id) + self.call_netflix_service({'method': 'remove_from_list', 'video_id': video_id}) return self.kodi_helper.refresh() @log @@ -343,7 +311,7 @@ class Navigation: video_list_id : :obj:`str` ID of the video list that should be displayed """ - self.netflix_session.add_to_list(video_id=video_id) + self.call_netflix_service({'method': 'add_to_list', 'video_id': video_id}) return self.kodi_helper.refresh() @log @@ -358,7 +326,7 @@ class Navigation: alt_title : :obj:`str` Alternative title (for the folder written to disc) """ - metadata = self.netflix_session.fetch_metadata(id=video_id) + metadata = self.call_netflix_service({'method': 'fetch_metadata', 'video_id': video_id}) # check for any errors if self._is_dirty_response(response=metadata): return False @@ -384,7 +352,7 @@ class Navigation: video_id : :obj:`str` ID of the movie or show """ - metadata = self.netflix_session.fetch_metadata(id=video_id) + metadata = self.call_netflix_service({'method': 'fetch_metadata', 'video_id': video_id}) # check for any errors if self._is_dirty_response(response=metadata): return False @@ -410,7 +378,8 @@ class Navigation: bool If we don't have an active session & the user couldn't be logged in """ - return True if self.netflix_session.is_logged_in(account=account) else self.netflix_session.login(account=account) + is_logged_in = self.call_netflix_service({'method': 'is_logged_in'}) + return True if is_logged_in else self.call_netflix_service({'method': 'login', 'email': account['email'], 'password': account['password']}) @log def before_routing_action (self, params): @@ -448,7 +417,7 @@ class Navigation: # check and switch the profile if needed if self.check_for_designated_profile_change(params=params): self.kodi_helper.invalidate_memcache() - self.netflix_session.switch_profile(profile_id=params['profile_id'], account=credentials) + self.call_netflix_service({'method': 'switch_profile', 'profile_id': params['profile_id']}) # check login, in case of main menu if 'action' not in params: self.establish_session(account=credentials) @@ -468,9 +437,10 @@ class Navigation: Profile should be switched or not """ # check if we need to switch the user - if 'guid' not in self.netflix_session.user_data: + user_data = self.call_netflix_service({'method': 'get_user_data'}) + if 'guid' not in user_data: return False - current_profile_id = self.netflix_session.user_data['guid'] + current_profile_id = user_data['guid'] return 'profile_id' in params and current_profile_id != params['profile_id'] def parse_paramters (self, paramstring): @@ -541,4 +511,40 @@ class Navigation: str Url + querystring based on the param """ - return self.base_url + '?' + urlencode(query) + return self.base_url + '?' + urllib.urlencode(query) + + def get_netflix_service_url (self): + """Returns URL & Port of the internal Netflix HTTP Proxy service + + Returns + ------- + str + Url + Port + """ + return 'http://localhost:' + str(self.kodi_helper.addon.getSetting('netflix_service_port')) + + def call_netflix_service (self, params): + """Makes a GET request to the internal Netflix HTTP proxy and returns the result + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + List of paramters to be url encoded + + Returns + ------- + :obj:`dict` + Netflix Service RPC result + """ + url_values = urllib.urlencode(params) + url = self.get_netflix_service_url() + full_url = url + '?' + url_values + data = urllib2.urlopen(full_url).read() + parsed_json = json.loads(data) + return parsed_json.get('result', None) + + def open_settings(self, url): + """Opens a foreign settings dialog""" + is_addon = self.kodi_helper.get_inputstream_addon() + url = is_addon if url == 'is' else url + return Addon(url).openSettings() diff --git a/resources/lib/NetflixHttpRequestHandler.py b/resources/lib/NetflixHttpRequestHandler.py new file mode 100644 index 0000000..3c2344f --- /dev/null +++ b/resources/lib/NetflixHttpRequestHandler.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Module: NetflixHttpRequestHandler +# Created on: 07.03.2017 + +import BaseHTTPServer +import json +from types import FunctionType +from urlparse import urlparse, parse_qs +from resources.lib.KodiHelper import KodiHelper +from resources.lib.NetflixSession import NetflixSession +from resources.lib.NetflixHttpSubRessourceHandler import NetflixHttpSubRessourceHandler + +kodi_helper = KodiHelper() + +netflix_session = NetflixSession( + cookie_path=kodi_helper.cookie_path, + data_path=kodi_helper.data_path, + verify_ssl=kodi_helper.get_ssl_verification_setting(), + log_fn=kodi_helper.log +) + +# get list of methods & instance form the sub ressource handler +methods = [x for x, y in NetflixHttpSubRessourceHandler.__dict__.items() if type(y) == FunctionType] +sub_res_handler = NetflixHttpSubRessourceHandler(kodi_helper=kodi_helper, netflix_session=netflix_session) + +class NetflixHttpRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """ Represents the callable internal server that dispatches requests to Netflix""" + + def do_GET(self): + """GET request handler (we only need this, as we only do GET requests internally)""" + url = urlparse(self.path) + params = parse_qs(url.query) + method = params.get('method', [None])[0] + + # not method given + if method == None: + self.send_error(500, 'No method declared') + return + + # no existing method given + if method not in methods: + self.send_error(404, 'Method "' + str(method) + '" not found. Available methods: ' + str(methods)) + return + + # call method & get the result + result = getattr(sub_res_handler, method)(params) + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'method': method, 'result': result})); + return + + def log_message(self, format, *args): + """Disable the BaseHTTPServer Log""" + return diff --git a/resources/lib/NetflixHttpSubRessourceHandler.py b/resources/lib/NetflixHttpSubRessourceHandler.py new file mode 100644 index 0000000..1c6d29e --- /dev/null +++ b/resources/lib/NetflixHttpSubRessourceHandler.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Module: NetflixHttpSubRessourceHandler +# Created on: 07.03.2017 + +class NetflixHttpSubRessourceHandler: + """ Represents the callable internal server routes & translates/executes them to requests for Netflix""" + + def __init__ (self, kodi_helper, netflix_session): + """Sets up credentials & video_list_cache cache + Assigns the netflix_session/kodi_helper instacnes + Does the initial login if we have user data + + Parameters + ---------- + kodi_helper : :obj:`KodiHelper` + instance of the KodiHelper class + + netflix_session : :obj:`NetflixSession` + instance of the NetflixSession class + """ + self.kodi_helper = kodi_helper + self.netflix_session = netflix_session + self.credentials = self.kodi_helper.get_credentials() + self.video_list_cache = {} + + # check if we have stored credentials, if so, do the login before the user requests it + # if that is done, we cache the profiles + if self.credentials['email'] != '' and self.credentials['password'] != '': + if self.netflix_session.is_logged_in(account=self.credentials): + self.netflix_session.refresh_session_data(account=self.credentials) + else: + self.netflix_session.login(account=self.credentials) + self.profiles = self.netflix_session.profiles + else: + self.profiles = [] + + def is_logged_in (self, params): + """Existing login proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + if self.credentials['email'] == '' or self.credentials['password'] == '': + return False + return self.netflix_session.is_logged_in(account=self.credentials) + + def logout (self, params): + """Logout proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + self.profiles = [] + self.credentials = {'email': '', 'password': ''} + return self.netflix_session.logout() + + def login (self, params): + """Logout proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + email = params.get('email', [''])[0] + password = params.get('password', [''])[0] + if email != '' and password != '': + self.credentials = {'email': email, 'password': password} + _ret = self.netflix_session.login(account=self.credentials) + self.profiles = self.netflix_session.profiles + return _ret + return None + + def list_profiles (self, params): + """Returns the cached list of profiles + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`dict` of :obj:`str` + List of profiles + """ + return self.profiles + + def get_esn (self, params): + """ESN getter function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`str` + Exracted ESN + """ + return self.netflix_session.esn + + def fetch_video_list_ids (self, params): + """Video list ids proxy function (caches video lists) + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`list` + Transformed response of the remote call + """ + cached_list = self.video_list_cache.get(self.netflix_session.user_data['guid'], None) + if cached_list != None: + self.kodi_helper.log('Serving cached list for user: ' + self.netflix_session.user_data['guid']) + return cached_list + video_list_ids_raw = self.netflix_session.fetch_video_list_ids() + if 'error' in video_list_ids_raw: + return video_list_ids_raw + return self.netflix_session.parse_video_list_ids(response_data=video_list_ids_raw) + + def fetch_video_list (self, params): + """Video list proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`list` + Transformed response of the remote call + """ + list_id = params.get('list_id', [''])[0] + raw_video_list = self.netflix_session.fetch_video_list(list_id=list_id) + if 'error' in raw_video_list: + return raw_video_list + # parse the video list ids + if 'videos' in raw_video_list.get('value', {}).keys(): + return self.netflix_session.parse_video_list(response_data=raw_video_list) + return [] + + def fetch_episodes_by_season (self, params): + """Episodes for season proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`list` + Transformed response of the remote call + """ + raw_episode_list = self.netflix_session.fetch_episodes_by_season(season_id=params.get('season_id')[0]) + if 'error' in raw_episode_list: + return raw_episode_list + return self.netflix_session.parse_episodes_by_season(response_data=raw_episode_list) + + def fetch_seasons_for_show (self, params): + """Season for show proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`list` + Transformed response of the remote call + """ + show_id = params.get('show_id', [''])[0] + raw_season_list = self.netflix_session.fetch_seasons_for_show(id=show_id) + if 'error' in raw_season_list: + return raw_season_list + # check if we have sesons, announced shows that are not available yet have none + if 'seasons' not in raw_season_list.get('value', {}): + return [] + return self.netflix_session.parse_seasons(id=show_id, response_data=raw_season_list) + + def rate_video (self, params): + """Video rating proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + video_id = params.get('video_id', [''])[0] + rating = params.get('rating', [''])[0] + return self.netflix_session.rate_video(video_id=video_id, rating=rating) + + def remove_from_list (self, params): + """Remove from my list proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + video_id = params.get('video_id', [''])[0] + return self.netflix_session.remove_from_list(video_id=video_id) + + def add_to_list (self, params): + """Add to my list proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + video_id = params.get('video_id', [''])[0] + return self.netflix_session.add_to_list(video_id=video_id) + + def fetch_metadata (self, params): + """Metadata proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + video_id = params.get('video_id', [''])[0] + return self.netflix_session.fetch_metadata(id=video_id) + + def switch_profile (self, params): + """Switch profile proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`Requests.Response` + Response of the remote call + """ + profile_id = params.get('profile_id', [''])[0] + return self.netflix_session.switch_profile(profile_id=profile_id, account=self.credentials) + + def get_user_data (self, params): + """User data getter function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`str` + Exracted User Data + """ + return self.netflix_session.user_data + + def search (self, params): + """Search proxy function + + Parameters + ---------- + params : :obj:`dict` of :obj:`str` + Request params + + Returns + ------- + :obj:`list` + Transformed response of the remote call + """ + term = params.get('term', [''])[0] + has_search_results = False + raw_search_results = self.netflix_session.fetch_search_results(search_str=term) + # check for any errors + if 'error' in raw_search_results: + return raw_search_results + + # determine if we found something + if 'search' in raw_search_results['value']: + for key in raw_search_results['value']['search'].keys(): + if self.netflix_session._is_size_key(key=key) == False: + has_search_results = raw_search_results['value']['search'][key]['titles']['length'] > 0 + if has_search_results == False: + if raw_search_results['value']['search'][key].get('suggestions', False) != False: + for entry in raw_search_results['value']['search'][key]['suggestions']: + if self.netflix_session._is_size_key(key=entry) == False: + if raw_search_results['value']['search'][key]['suggestions'][entry]['relatedvideos']['length'] > 0: + has_search_results = True + + # display that we haven't found a thing + if has_search_results == False: + return [] + + # list the search results + search_results = self.netflix_session.parse_search_results(response_data=raw_search_results) + # add more menaingful data to the search results + raw_search_contents = self.netflix_session.fetch_video_list_information(video_ids=search_results.keys()) + # check for any errors + if 'error' in raw_search_contents: + return raw_search_contents + return self.netflix_session.parse_video_list(response_data=raw_search_contents) diff --git a/resources/lib/NetflixSession.py b/resources/lib/NetflixSession.py index 0ad98b1..2dc142b 100644 --- a/resources/lib/NetflixSession.py +++ b/resources/lib/NetflixSession.py @@ -6,7 +6,7 @@ import os import json from requests import session, cookies -from urllib import quote +from urllib import quote, unquote from time import time from base64 import urlsafe_b64encode from bs4 import BeautifulSoup, SoupStrainer @@ -24,10 +24,10 @@ class NetflixSession: urls = { 'login': '/login', - 'browse': '/browse', - 'video_list_ids': '/warmer', + 'browse': '/profiles/manage', + 'video_list_ids': '/preflight', 'shakti': '/pathEvaluator', - 'profiles': '/browse', + 'profiles': '/profiles/manage', 'switch_profiles': '/profiles/switch', 'adult_pin': '/pin/service', 'metadata': '/metadata', @@ -280,14 +280,9 @@ class NetflixSession: if response.status_code != 200: return False - # fetch the index page again, so that we can fetch the corresponding user data - browse_response = self._session_get(component='browse') - only_script_tags = SoupStrainer('script') - browse_soup = BeautifulSoup(browse_response.text, 'html.parser', parse_only=only_script_tags) account_hash = self._generate_account_hash(account=account) self.user_data['guid'] = profile_id; - self._save_data(filename=self.data_path + '_' + account_hash) - return True + return self._save_data(filename=self.data_path + '_' + account_hash) def send_adult_pin (self, pin): """Send the adult pin to Netflix in case an adult rated video requests it @@ -1298,6 +1293,13 @@ class NetflixSession: '_': int(time()), 'authURL': self.user_data['authURL'] } + + # check if we have a root lolomo for that user within our cookies + for cookie in self.session.cookies: + if cookie.name == 'lhpuuidh-browse-' + self.user_data['guid']: + value = unquote(cookie.value) + payload['lolomoid'] = value[value.rfind(':')+1:]; + response = self._session_get(component='video_list_ids', params=payload, type='api') return self._process_response(response=response, component=self._get_api_url_for(component='video_list_ids')) @@ -1568,8 +1570,6 @@ class NetflixSession: }) params = { - 'withSize': True, - 'materialize': True, 'model': self.user_data['gpsModel'] } @@ -2296,5 +2296,5 @@ class NetflixSession: self.esn = self._parse_esn_data(netflix_page_data=netflix_page_data) self.api_data = self._parse_api_base_data(netflix_page_data=netflix_page_data) self.profiles = self._parse_profile_data(netflix_page_data=netflix_page_data) - self.log('Found ESN "' + self.esn) + self.log('Found ESN "' + self.esn + '"') return netflix_page_data diff --git a/resources/settings.xml b/resources/settings.xml index 362b080..5c0faab 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -4,6 +4,7 @@ + @@ -13,8 +14,9 @@ - + + diff --git a/service.py b/service.py index 1829fbd..5843601 100644 --- a/service.py +++ b/service.py @@ -1,17 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Module: service +# Created on: 26.01.2017 + import threading import SocketServer import xbmc -import xbmcaddon import socket +from xbmcaddon import Addon from resources.lib.KodiHelper import KodiHelper from resources.lib.MSLHttpRequestHandler import MSLHttpRequestHandler +from resources.lib.NetflixHttpRequestHandler import NetflixHttpRequestHandler -addon = xbmcaddon.Addon() -kodi_helper = KodiHelper( - plugin_handle=None, - base_url=None -) - +# helper function to select an unused port on the host machine def select_unused_port(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) @@ -19,28 +20,60 @@ def select_unused_port(): sock.close() return port -port = select_unused_port() -addon.setSetting('msl_service_port', str(port)) -kodi_helper.log(msg='Picked Port: ' + str(port)) +addon = Addon() +kodi_helper = KodiHelper() + +# pick & store a port for the MSL service +msl_port = select_unused_port() +addon.setSetting('msl_service_port', str(msl_port)) +kodi_helper.log(msg='[MSL] Picked Port: ' + str(msl_port)) -#Config the HTTP Server +# pick & store a port for the internal Netflix HTTP proxy service +ns_port = select_unused_port() +addon.setSetting('netflix_service_port', str(ns_port)) +kodi_helper.log(msg='[NS] Picked Port: ' + str(ns_port)) + +# server defaults SocketServer.TCPServer.allow_reuse_address = True -server = SocketServer.TCPServer(('127.0.0.1', port), MSLHttpRequestHandler) -server.server_activate() -server.timeout = 1 + +# configure the MSL Server +msl_server = SocketServer.TCPServer(('127.0.0.1', msl_port), MSLHttpRequestHandler) +msl_server.server_activate() +msl_server.timeout = 1 + +# configure the Netflix Data Server +nd_server = SocketServer.TCPServer(('127.0.0.1', ns_port), NetflixHttpRequestHandler) +nd_server.server_activate() +nd_server.timeout = 1 if __name__ == '__main__': monitor = xbmc.Monitor() - thread = threading.Thread(target=server.serve_forever) - thread.daemon = True - thread.start() + # start thread for MLS servie + msl_thread = threading.Thread(target=msl_server.serve_forever) + msl_thread.daemon = True + msl_thread.start() + + # start thread for Netflix HTTP service + nd_thread = threading.Thread(target=nd_server.serve_forever) + nd_thread.daemon = True + nd_thread.start() + + # kill the services if kodi monitor tells us to while not monitor.abortRequested(): if monitor.waitForAbort(5): - server.shutdown() + msl_server.shutdown() + nd_server.shutdown() break - server.server_close() - server.socket.close() - server.shutdown() + # MSL service shutdown sequence + msl_server.server_close() + msl_server.socket.close() + msl_server.shutdown() kodi_helper.log(msg='Stopped MSL Service') + + # Netflix service shutdown sequence + nd_server.server_close() + nd_server.socket.close() + nd_server.shutdown() + kodi_helper.log(msg='Stopped HTTP Service')