feat(core): Add license, adapt to API changes, fix issues with KIDS profiles, fix...
authorSebastian Golasch <public@asciidisco.com>
Thu, 16 Mar 2017 15:20:20 +0000 (16:20 +0100)
committerSebastian Golasch <public@asciidisco.com>
Thu, 16 Mar 2017 15:20:20 +0000 (16:20 +0100)
LICENSE.txt [new file with mode: 0644]
addon.xml
resources/language/English/strings.po
resources/language/German/strings.po
resources/language/Slovak/strings.po
resources/language/Spanish/strings.po
resources/lib/Navigation.py
resources/lib/NetflixHttpSubRessourceHandler.py
resources/lib/NetflixSession.py
service.py

diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..e3fa8b2
--- /dev/null
@@ -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.
index 3bce2b8386f27f81c9c312ef3d73cc6f574bf5ea..9cdf2344996359b2c262d90530834f19573aa940 100644 (file)
--- a/addon.xml
+++ b/addon.xml
@@ -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"/>
     </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>
index 128f5459a22de78e53b45f125e39cfd29d5608ce..1d452549c1bbb7e6a72f1fee91e01af0c8ab8428 100644 (file)
@@ -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 ""
index 1d8d3a605db1fb653e390af253759ec7c586e7da..515cc5f6392b5a21c50d26e72afb5dd26fafe07b 100644 (file)
@@ -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 ""
index 2654d7133098bb1ba5e1d8fb6a41b4c120d64013..527d29db8064b4e588f8340d51cc818c90049b95 100644 (file)
@@ -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 ""
index 572f8aa4be173a1ca35d4cc5b4820bdbbaf23a9b..b841a3b08d4e7c316a7380848c68e06ef3bcce7b 100644 (file)
@@ -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 ""
index c2ee4cc05396c692c15c2275b62ba6d8deb19e7a..1c9a9ab97aa08ff4cb2b99db803512e244e9af66 100644 (file)
@@ -145,7 +145,8 @@ class Navigation:
         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
@@ -182,17 +183,11 @@ class Navigation:
         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:
@@ -215,19 +210,14 @@ class Navigation:
         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:
@@ -246,40 +236,29 @@ class Navigation:
         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
@@ -433,7 +412,11 @@ class Navigation:
         # 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)
@@ -454,9 +437,12 @@ class Navigation:
         """
         # 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):
@@ -553,11 +539,19 @@ class Navigation:
             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"""
index 4e7e1dd0b2659b399edb319b3fac80009d546335..3c6e7e425869849870f468b068a70e8b61489852 100644 (file)
@@ -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"""
 
@@ -22,19 +24,29 @@ class NetflixHttpSubRessourceHandler:
         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
@@ -366,3 +378,16 @@ class NetflixHttpSubRessourceHandler:
         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
index 6f28de148264c7a2fbe0107e0ecd0886c6e9b038..7fcea384d88b330b07cc1c9658b201f387071f9e 100644 (file)
@@ -150,12 +150,12 @@ class NetflixSession:
                 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):
@@ -769,7 +769,7 @@ class NetflixSession:
                 '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'],
@@ -1248,7 +1248,7 @@ class NetflixSession:
                 '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'],
@@ -1935,7 +1935,7 @@ class NetflixSession:
         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={}):
@@ -1960,8 +1960,12 @@ class NetflixSession:
         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):
@@ -2333,5 +2337,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(msg='Found ESN "' + self.esn + '"')
         return netflix_page_data
index 584360115ee568c6d881c8970447d770ef5d14ee..92a2cda0cf36124d6deae81dd4db09bab5eac971 100644 (file)
@@ -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
@@ -47,7 +47,7 @@ nd_server.server_activate()
 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)