Skip to content

Commit

Permalink
feat(core): Add license, adapt to API changes, fix issues with KIDS p…
Browse files Browse the repository at this point in the history
…rofiles, fix issues with prefetching and network unavailability, fix issues with cached lists between profiles
  • Loading branch information
asciidisco committed Mar 16, 2017
1 parent 9e88e0f commit ac8e493
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 79 deletions.
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 Sebastian Golasch

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
11 changes: 9 additions & 2 deletions addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.netflix" name="Netflix" version="0.11.2" provider-name="libdev + jojo + asciidisco">
<addon id="plugin.video.netflix" name="Netflix" version="0.11.3" provider-name="libdev + jojo + asciidisco">
<requires>
<import addon="xbmc.python" version="2.24.0"/>
<import addon="script.module.beautifulsoup4" version="4.3.2"/>
Expand All @@ -26,7 +26,14 @@
</assets>
<platform>all</platform>
<license>MIT</license>
<forum>http://www.kodinerds.net/</forum>
<forum>http://www.kodinerds.net/index.php/Thread/55607-Inputstream-Agile-Betatest-Netflix/</forum>
<source>https://github.com/asciidisco/plugin.video.netflix</source>
<news>v0.11.3 (2017-3-16)
- Added spanish and slowenian language files
- Added license file
- Adapted to latest API changes from Netflix
- Fixed issues with entering KIDS profiles
- Fixed prefetching started before network was available
- Fixed issues with cached lists when switching profiles</news>
</extension>
</addon>
2 changes: 1 addition & 1 deletion resources/language/English/strings.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Kodi Media Center language file
# Addon Name: Netflix
# Addon id: plugin.video.netflix
# Addon version: 0.11.2
# Addon version: 0.11.3
# Addon Provider: libdev + jojo + asciidisco
msgid ""
msgstr ""
Expand Down
2 changes: 1 addition & 1 deletion resources/language/German/strings.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Kodi Media Center language file
# Addon Name: Netflix
# Addon id: plugin.video.netflix
# Addon version: 0.11.2
# Addon version: 0.11.3
# Addon Provider: libdev + jojo + asciidisco
msgid ""
msgstr ""
Expand Down
2 changes: 1 addition & 1 deletion resources/language/Slovak/strings.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Kodi Media Center language file
# Addon Name: Netflix
# Addon id: plugin.video.netflix
# Addon version: 0.11.2
# Addon version: 0.11.3
# Addon Provider: libdev + jojo + asciidisco
msgid ""
msgstr ""
Expand Down
2 changes: 1 addition & 1 deletion resources/language/Spanish/strings.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Kodi Media Center language file
# Addon Name: Netflix
# Addon id: plugin.video.netflix
# Addon version: 0.11.2
# Addon version: 0.11.3
# Addon Provider: libdev + jojo + asciidisco
msgid ""
msgstr ""
Expand Down
104 changes: 49 additions & 55 deletions resources/lib/Navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ def show_search_results (self, term):
bool
If no results are available
"""
search_contents = self.call_netflix_service({'method': 'search', 'term': term})
user_data = self.call_netflix_service({'method': 'get_user_data'})
search_contents = self.call_netflix_service({'method': 'search', 'term': term, 'guid': user_data['guid'], 'cache': True})
# check for any errors
if self._is_dirty_response(response=search_contents):
return False
Expand Down Expand Up @@ -182,17 +183,11 @@ def show_episode_list (self, season_id):
season_id : :obj:`str`
ID of the season episodes should be displayed for
"""
cache_id = 'episodes_' + season_id
if self.kodi_helper.has_cached_item(cache_id=cache_id):
episode_list = self.kodi_helper.get_cached_item(cache_id=cache_id)
else:
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=episode_list):
return False
# parse the raw Netflix data
self.kodi_helper.add_cached_item(cache_id=cache_id, contents=episode_list)

user_data = self.call_netflix_service({'method': 'get_user_data'})
episode_list = self.call_netflix_service({'method': 'fetch_episodes_by_season', 'season_id': season_id, 'guid': user_data['guid'], 'cache': True})
# check for any errors
if self._is_dirty_response(response=episode_list):
return False
# sort seasons by number (they´re coming back unsorted from the api)
episodes_sorted = []
for episode_id in episode_list:
Expand All @@ -215,19 +210,14 @@ def show_seasons (self, show_id):
bool
If no seasons are available
"""
cache_id = 'season_' + show_id
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 = 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):
return False
# check if we have sesons, announced shows that are not available yet have none
if len(season_list) == 0:
return self.kodi_helper.build_no_seasons_available()
# parse the seasons raw response from Netflix
self.kodi_helper.add_cached_item(cache_id=cache_id, contents=season_list)
user_data = self.call_netflix_service({'method': 'get_user_data'})
season_list = self.call_netflix_service({'method': 'fetch_seasons_for_show', 'show_id': show_id, 'guid': user_data['guid'], 'cache': True})
# check for any errors
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 len(season_list) == 0:
return self.kodi_helper.build_no_seasons_available()
# sort seasons by index by default (they´re coming back unsorted from the api)
seasons_sorted = []
for season_id in season_list:
Expand All @@ -246,40 +236,29 @@ def show_video_list (self, video_list_id, type):
type : :obj:`str`
None or 'queue' f.e. when it´s a special video lists
"""
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:
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=video_list):
return False
# parse the video list ids
if len(video_list) > 0:
self.kodi_helper.add_cached_item(cache_id=video_list_id, contents=video_list)
user_data = self.call_netflix_service({'method': 'get_user_data'})
video_list = self.call_netflix_service({'method': 'fetch_video_list', 'list_id': video_list_id, 'guid': user_data['guid'] ,'cache': True})
# check for any errors
if self._is_dirty_response(response=video_list):
return False
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)

def show_video_lists (self):
"""List the users video lists (recommendations, my list, etc.)"""
cache_id='main_menu'
if self.kodi_helper.has_cached_item(cache_id=cache_id):
video_list_ids = self.kodi_helper.get_cached_item(cache_id=cache_id)
# determine if we´re in Kids profile mode
user_data = self.call_netflix_service({'method': 'get_user_data'})
profiles = self.call_netflix_service({'method': 'list_profiles'})
is_kids = profiles.get(user_data['guid']).get('isKids', False)
# fetch video lists
if is_kids == True:
video_list_ids = self.call_netflix_service({'method': 'fetch_video_list_ids_for_kids', 'guid': user_data['guid'], 'cache': True})
else:
# determine if we´re in Kids profile mode
user_data = self.call_netflix_service({'method': 'get_user_data'})
profiles = self.call_netflix_service({'method': 'list_profiles'})
is_kids = profiles.get(user_data['guid']).get('isKids', False)
# fetch video lists
if is_kids == True:
video_list_ids = self.call_netflix_service({'method': 'fetch_video_list_ids_for_kids'})
else:
video_list_ids = self.call_netflix_service({'method': 'fetch_video_list_ids'})

# check for any errors
if self._is_dirty_response(response=video_list_ids):
return False
# cache the video list ids
#self.kodi_helper.add_cached_item(cache_id=cache_id, contents=video_list_ids)
video_list_ids = self.call_netflix_service({'method': 'fetch_video_list_ids', 'guid': user_data['guid'], 'cache': True})

# check for any errors
if self._is_dirty_response(response=video_list_ids):
return False
# 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']
# define where to route the user
Expand Down Expand Up @@ -433,7 +412,11 @@ def before_routing_action (self, params):
# check and switch the profile if needed
if self.check_for_designated_profile_change(params=params):
self.kodi_helper.invalidate_memcache()
self.call_netflix_service({'method': 'switch_profile', 'profile_id': params['profile_id']})
profile_id = params.get('profile_id', None)
if profile_id == None:
user_data = self.call_netflix_service({'method': 'get_user_data'})
profile_id = user_data['guid']
self.call_netflix_service({'method': 'switch_profile', 'profile_id': profile_id})
# check login, in case of main menu
if 'action' not in params:
self.establish_session(account=credentials)
Expand All @@ -454,9 +437,12 @@ def check_for_designated_profile_change (self, params):
"""
# check if we need to switch the user
user_data = self.call_netflix_service({'method': 'get_user_data'})
profiles = self.call_netflix_service({'method': 'list_profiles'})
if 'guid' not in user_data:
return False
current_profile_id = user_data['guid']
if profiles.get(current_profile_id).get('isKids', False) == True:
return True
return 'profile_id' in params and current_profile_id != params['profile_id']

def parse_paramters (self, paramstring):
Expand Down Expand Up @@ -553,11 +539,19 @@ def call_netflix_service (self, params):
Netflix Service RPC result
"""
url_values = urllib.urlencode(params)
# check for cached items
if self.kodi_helper.has_cached_item(cache_id=url_values) and params.get('cache', False) == True:
self.log(msg='Fetching item from cache: (cache_id=' + url_values + ')')
return self.kodi_helper.get_cached_item(cache_id=url_values)
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)
result = parsed_json.get('result', None)
if params.get('cache', False) == True:
self.log(msg='Adding item to cache: (cache_id=' + url_values + ')')
self.kodi_helper.add_cached_item(cache_id=url_values, contents=result)
return result

def open_settings(self, url):
"""Opens a foreign settings dialog"""
Expand Down
41 changes: 33 additions & 8 deletions resources/lib/NetflixHttpSubRessourceHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Module: NetflixHttpSubRessourceHandler
# Created on: 07.03.2017

from urllib2 import urlopen, URLError

class NetflixHttpSubRessourceHandler:
""" Represents the callable internal server routes & translates/executes them to requests for Netflix"""

Expand All @@ -22,19 +24,29 @@ def __init__ (self, kodi_helper, netflix_session):
self.kodi_helper = kodi_helper
self.netflix_session = netflix_session
self.credentials = self.kodi_helper.get_credentials()
self.profiles = []
self.prefetch_login()
self.video_list_cache = {}
self.lolomo = None

# 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)
def prefetch_login (self):
"""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._network_availble():
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)
self.profiles = self.netflix_session.profiles
else:
self.netflix_session.login(account=self.credentials)
self.profiles = self.netflix_session.profiles
else:
self.netflix_session.login(account=self.credentials)
self.profiles = self.netflix_session.profiles
self.profiles = []
else:
self.profiles = []
sleep(1)
self.prefetch_login()

def is_logged_in (self, params):
"""Existing login proxy function
Expand Down Expand Up @@ -366,3 +378,16 @@ def search (self, params):
if 'error' in raw_search_contents:
return raw_search_contents
return self.netflix_session.parse_video_list(response_data=raw_search_contents)

def _network_availble(self):
"""Check if the network is available
Returns
-------
bool
Network can be accessed
"""
try:
urlopen('http://216.58.192.142', timeout=1)
return True
except URLError as err:
return False
20 changes: 12 additions & 8 deletions resources/lib/NetflixSession.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,12 @@ def extract_inline_netflix_page_data (self, page_soup):
List of all the serialized data pulled out of the pagws <script/> tags
"""
scripts = page_soup.find_all('script', attrs={'src': None});
self.log('Trying sloppy inline data parser')
self.log(msg='Trying sloppy inline data parser')
inline_data = self._sloppy_parse_inline_data(scripts=scripts)
if self._verfify_auth_and_profiles_data(data=inline_data) != False:
self.log('Sloppy inline data parsing successfull')
self.log(msg='Sloppy inline data parsing successfull')
return inline_data
self.log('Sloppy inline parser failed, trying JS parser')
self.log(msg='Sloppy inline parser failed, trying JS parser')
return self._accurate_parse_inline_data(scripts=scripts)

def is_logged_in (self, account):
Expand Down Expand Up @@ -769,7 +769,7 @@ def parse_video_list_entry (self, id, list_id, video, persons, genres):
'synopsis': video['synopsis'],
'regular_synopsis': video['regularSynopsis'],
'type': video['summary']['type'],
'rating': video['userRating']['average'],
'rating': video['userRating'].get('average', 0) if video['userRating'].get('average', None) != None else video['userRating'].get('predicted', 0),
'episode_count': season_info['episode_count'],
'seasons_label': season_info['seasons_label'],
'seasons_count': season_info['seasons_count'],
Expand Down Expand Up @@ -1248,7 +1248,7 @@ def parse_episode (self, episode, genres=None):
'mpaa': str(episode['maturity']['rating']['board']) + ' ' + str(episode['maturity']['rating']['value']),
'maturity': episode['maturity'],
'playcount': (0, 1)[episode['watched']],
'rating': episode['userRating']['average'],
'rating': episode['userRating'].get('average', 0) if episode['userRating'].get('average', None) != None else episode['userRating'].get('predicted', 0),
'thumb': episode['info']['interestingMoments']['url'],
'fanart': episode['interestingMoment']['_1280x720']['jpg']['url'],
'poster': episode['boxarts']['_1280x720']['jpg']['url'],
Expand Down Expand Up @@ -1935,7 +1935,7 @@ def _session_post (self, component, type='document', data={}, headers={}, params
start = time()
response = self.session.post(url=url, data=data, params=params, headers=headers, verify=self.verify_ssl)
end = time()
self.log('[POST] Request for "' + url + '" took ' + str(end - start) + ' seconds')
self.log(msg='[POST] Request for "' + url + '" took ' + str(end - start) + ' seconds')
return response

def _session_get (self, component, type='document', params={}):
Expand All @@ -1960,8 +1960,12 @@ def _session_get (self, component, type='document', params={}):
url = self._get_document_url_for(component=component) if type == 'document' else self._get_api_url_for(component=component)
start = time()
response = self.session.get(url=url, verify=self.verify_ssl, params=params)
for cookie in response.cookies:
if cookie.name.find('lhpuuidh-browse-' + self.user_data['guid']) != -1 and cookie.name.rfind('-T') == -1:
start = unquote(cookie.value).rfind(':')
return unquote(cookie.value)[start+1:]
end = time()
self.log('[GET] Request for "' + url + '" took ' + str(end - start) + ' seconds')
self.log(msg='[GET] Request for "' + url + '" took ' + str(end - start) + ' seconds')
return response

def _sloppy_parse_user_and_api_data (self, key, contents):
Expand Down Expand Up @@ -2333,5 +2337,5 @@ def _parse_page_contents (self, page_soup):
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(msg='Found ESN "' + self.esn + '"')
return netflix_page_data
4 changes: 2 additions & 2 deletions service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import threading
import SocketServer
import xbmc
import socket
from xbmc import Monitor
from xbmcaddon import Addon
from resources.lib.KodiHelper import KodiHelper
from resources.lib.MSLHttpRequestHandler import MSLHttpRequestHandler
Expand Down Expand Up @@ -47,7 +47,7 @@ def select_unused_port():
nd_server.timeout = 1

if __name__ == '__main__':
monitor = xbmc.Monitor()
monitor = Monitor()

# start thread for MLS servie
msl_thread = threading.Thread(target=msl_server.serve_forever)
Expand Down

0 comments on commit ac8e493

Please sign in to comment.