feat(core): Adds library export for movies & shows, fixes for search, fixes for displ...
authorSebastian Golasch <public@asciidisco.com>
Fri, 3 Feb 2017 16:07:44 +0000 (17:07 +0100)
committerSebastian Golasch <public@asciidisco.com>
Fri, 3 Feb 2017 16:07:44 +0000 (17:07 +0100)
addon.py
addon.xml
resources/language/English/strings.po
resources/language/German/strings.po
resources/lib/KodiHelper.py
resources/lib/Library.py
resources/lib/Navigation.py
resources/lib/NetflixSession.py
resources/settings.xml

index 80355f325cae0afd75221e2093d27df83384a3f9..9303b974e9c3dbdd34417d4f72efb665a5fa7d27 100644 (file)
--- a/addon.py
+++ b/addon.py
@@ -30,10 +30,10 @@ 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
 )
 library = Library(
-    base_url=base_url,
     root_folder=kodi_helper.base_data_path,
     library_settings=kodi_helper.get_custom_library_settings(),
     log_fn=kodi_helper.log
index 140b595f502c0f5f5120bf65420547a451ab4e83..f797fef513d7ca081f5d080097e90a481b86132a 100644 (file)
--- a/addon.xml
+++ b/addon.xml
@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<addon id="plugin.video.netflix" name="Netflix" version="0.2.0" provider-name="tba">
+<addon id="plugin.video.netflix" name="Netflix" version="0.3.0" provider-name="tba">
   <requires>
     <import addon="xbmc.python" version="2.24.0"/>
-    <import addon="script.module.beautifulsoup" version="3.2.1"/>
+    <import addon="script.module.beautifulsoup4" version="4.3.2"/>
     <import addon="script.module.requests" version="2.12.4"/>
     <import addon="script.module.pycryptodome" version="3.4.3"/>
     <import addon="script.module.simplejson" version="3.3.0"/>
-    <import addon="inputstream.adaptive" version="1.0.6"/>
+    <import addon="inputstream.adaptive" version="1.0.7"/>
   </requires>
   <extension point="xbmc.python.pluginsource" library="addon.py">
     <provides>video</provides>
index 100503117a3e24abb27fb7e8f1c2ccc33ea35c24..eb5e3785fa2e5eac2df3d0bb42b3efa40d312831 100644 (file)
@@ -148,3 +148,7 @@ msgstr ""
 msgctxt "#30030"
 msgid "Remove from library"
 msgstr ""
+
+msgctxt "#30031"
+msgid "Change library title"
+msgstr ""
index 671cc8678edd500b018de455aa9c3ec3520c29ff..d82af9c079a36b6ce17468b79cd6a90dd786f2fc 100644 (file)
@@ -148,3 +148,7 @@ msgstr "Inputstream nicht gefunden"
 msgctxt "#30030"
 msgid "Remove from library"
 msgstr "Aus Bibliothek entfernen"
+
+msgctxt "#30031"
+msgid "Change library title"
+msgstr "Export titel ändern"
index ff4e8360755b392f7dc2ecac28618704b0b168f8..44d992bfae5ad67b678a22339b833748e907a233 100644 (file)
@@ -4,24 +4,23 @@
 # Created on: 13.01.2017
 
 import os
+import urllib
 import xbmcplugin
 import xbmcgui
 import xbmcaddon
 import xbmc
 import json
+try:
+   import cPickle as pickle
+except:
+   import pickle
 
 class KodiHelper:
     """Consumes all the configuration data from Kodi as well as turns data into lists of folders and videos"""
 
-    msl_service_server_certificate = 'Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eAGuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRITgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95OEtzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vfS6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0nu39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlGGCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqVDXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nAovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ0fFdCmw='
-    """str: MSL service certificate"""
-
     msl_service_server_url = 'http://localhost:%PORT%'
     """str: MSL service url"""
 
-    msl_service_port = 8000
-    """str: MSL service port (TODO: Make it dynamic)"""
-
     def __init__ (self, plugin_handle, base_url):
         """Fetches all needed info from Kodi & configures the baseline of the plugin
 
@@ -47,6 +46,7 @@ class KodiHelper:
         self.default_fanart = self.addon.getAddonInfo('fanart')
         self.win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
         self.library = None
+        self.setup_memcache()
 
     def refresh (self):
         """Refrsh the current list"""
@@ -79,11 +79,30 @@ class KodiHelper:
 
         Returns
         -------
-        term : :obj:`str`
+        :obj:`str`
             Term to search for
         """
         dlg = xbmcgui.Dialog()
-        return dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
+        term = dlg.input(self.get_local_string(string_id=30003), type=xbmcgui.INPUT_ALPHANUM)
+        if len(term) == 0:
+            term = ' '
+        return term
+
+    def show_add_to_library_title_dialog (self, original_title):
+        """Asks the user for an alternative title for the show/movie that gets exported to the local library
+
+        Parameters
+        ----------
+        original_title : :obj:`str`
+            Original title of the show (as suggested by the addon)
+
+        Returns
+        -------
+        :obj:`str`
+            Title to persist
+        """
+        dlg = xbmcgui.Dialog()
+        return dlg.input(heading=self.get_local_string(string_id=30031), defaultt=original_title, type=xbmcgui.INPUT_ALPHANUM)
 
     def show_password_dialog (self):
         """Asks the user for its Netflix password
@@ -143,7 +162,7 @@ class KodiHelper:
         dialog.notification(self.get_local_string(string_id=30028), self.get_local_string(string_id=30029), xbmcgui.NOTIFICATION_ERROR, 5000)
         return True
 
-    def set_setting (key, value):
+    def set_setting (self, key, value):
         """Public interface for the addons setSetting method
 
         Returns
@@ -179,12 +198,96 @@ class KodiHelper:
             'customlibraryfolder': self.addon.getSetting('customlibraryfolder')
         }
 
+    def get_ssl_verification_setting (self):
+        """Returns the setting that describes if we should verify the ssl transport when loading data
+
+        Returns
+        -------
+        bool
+            Verify or not
+        """
+        return self.addon.getSetting('ssl_verification') == 'true'
+
     def set_main_menu_selection (self, type):
+        """Persist the chosen main menu entry in memory
+
+        Parameters
+        ----------
+        type : :obj:`str`
+            Selected menu item
+        """
         self.win.setProperty('main_menu_selection', type)
 
     def get_main_menu_selection (self):
+        """Gets the persisted chosen main menu entry from memory
+
+        Returns
+        -------
+        :obj:`str`
+            The last chosen main menu entry
+        """
         return self.win.getProperty('main_menu_selection')
 
+    def setup_memcache (self):
+        """Sets up the memory cache if not existant"""
+        cached_items = self.win.getProperty('memcache')
+        # no cache setup yet, create one
+        if len(cached_items) < 1:
+            self.win.setProperty('memcache', pickle.dumps({}))
+
+    def invalidate_memcache (self):
+        """Invalidates the memory cache"""
+        self.win.setProperty('memcache', pickle.dumps({}))
+
+    def has_cached_item (self, cache_id):
+        """Checks if the requested item is in memory cache
+
+        Parameters
+        ----------
+        cache_id : :obj:`str`
+            ID of the cache entry
+
+        Returns
+        -------
+        bool
+            Item is cached
+        """
+        cached_items = pickle.loads(self.win.getProperty('memcache'))
+        return cache_id in cached_items.keys()
+
+    def get_cached_item (self, cache_id):
+        """Returns an item from the in memory cache
+
+        Parameters
+        ----------
+        cache_id : :obj:`str`
+            ID of the cache entry
+
+        Returns
+        -------
+        mixed
+            Contents of the requested cache item or none
+        """
+        cached_items = pickle.loads(self.win.getProperty('memcache'))
+        if self.has_cached_item(cache_id) != True:
+            return None
+        return cached_items[cache_id]
+
+    def add_cached_item (self, cache_id, contents):
+        """Adds an item to the in memory cache
+
+        Parameters
+        ----------
+        cache_id : :obj:`str`
+            ID of the cache entry
+
+        contents : mixed
+            Cache entry contents
+        """
+        cached_items = pickle.loads(self.win.getProperty('memcache'))
+        cached_items.update({cache_id: contents})
+        self.win.setProperty('memcache', pickle.dumps(cached_items))
+
     def build_profiles_listing (self, profiles, action, build_url):
         """Builds the profiles list Kodi screen
 
@@ -288,16 +391,12 @@ class KodiHelper:
 
         # (re)select the previously selected main menu entry
         idx = 1
-        preselected_list_item = None
         for item in preselect_items:
             idx += 1
-            if item:
-                preselected_list_item = idx
-        if self.get_main_menu_selection() == 'search':
-            preselected_list_item = idx + 2
+            preselected_list_item = idx if item else None
+        preselected_list_item = idx + 1 if self.get_main_menu_selection() == 'search' else preselected_list_item
         if preselected_list_item != None:
-            xbmc.executebuiltin('SetFocus(%s, %s)' % (self.win.getFocusId(), preselected_list_item))
-
+            xbmc.executebuiltin('ActivateWindowAndFocus(%s, %s)' % (str(self.win.getFocusId()), str(preselected_list_item)))
         return True
 
     def build_video_listing (self, video_list, actions, type, build_url):
@@ -344,6 +443,10 @@ class KodiHelper:
                 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)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_GENRE)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
         xbmcplugin.endOfDirectory(self.plugin_handle)
         return True
 
@@ -467,6 +570,10 @@ class KodiHelper:
                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=True)
 
         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
         xbmcplugin.endOfDirectory(self.plugin_handle)
         return True
 
@@ -503,7 +610,14 @@ class KodiHelper:
                     needs_pin = (True, False)[int(episode['maturity']['rating']['maturityLevel']) >= 1000]
                     url = build_url({'action': 'play_video', 'video_id': episode_id, 'pin': needs_pin, 'start_offset': episode['bookmark']})
                     xbmcplugin.addDirectoryItem(handle=self.plugin_handle, url=url, listitem=li, isFolder=False)
+
         xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_EPISODE)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_NONE)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_VIDEO_YEAR)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LABEL)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_LASTPLAYED)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_TITLE)
+        xbmcplugin.addSortMethod(handle=self.plugin_handle, sortMethod=xbmcplugin.SORT_METHOD_DURATION)
         xbmcplugin.endOfDirectory(self.plugin_handle)
         return True
 
@@ -533,12 +647,12 @@ class KodiHelper:
             return False
 
         # inputstream addon properties
-        msl_service_url = self.msl_service_server_url.replace('%PORT%', str(self.msl_service_port))
+        msl_service_url = self.msl_service_server_url.replace('%PORT%', str(self.addon.getSetting('msl_service_port')))
         play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
         play_item.setProperty(inputstream_addon + '.license_type', 'com.widevine.alpha')
         play_item.setProperty(inputstream_addon + '.manifest_type', 'mpd')
         play_item.setProperty(inputstream_addon + '.license_key', msl_service_url + '/license?id=' + video_id + '||b{SSM}!b{SID}|')
-        play_item.setProperty(inputstream_addon + '.server_certificate', self.msl_service_server_certificate)
+        play_item.setProperty(inputstream_addon + '.server_certificate', self.addon.getSetting('msl_service_certificate'))
         play_item.setProperty('inputstreamaddon', inputstream_addon)
 
         # check if we have a bookmark e.g. start offset position
@@ -679,7 +793,8 @@ class KodiHelper:
         entry_keys = entry.keys()
 
         # action item templates
-        url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + ')'
+        encoded_title = urllib.urlencode({'title': entry['title'].encode('utf-8')}) if 'title' in entry else ''
+        url_tmpl = 'XBMC.RunPlugin(' + self.base_url + '?action=%action%&id=' + str(entry['id']) + '&' + encoded_title + ')'
         actions = [
             ['export_to_library', self.get_local_string(30018), 'export'],
             ['remove_from_library', self.get_local_string(30030), 'remove'],
@@ -703,10 +818,15 @@ class KodiHelper:
         items.append(action['rate_on_netflix'])
 
         # add possibility to export this movie/show/season/episode to a static/local library (and to remove it)
-        # TODO: Not yet finished, still needs implementation
-        #if 'type' in entry_keys:
-            #items.append(action['export_to_library'])
-            #items.append(action['remove_from_library'])
+        if 'type' in entry_keys:
+            # add/remove movie
+            if entry['type'] == 'movie':
+                action_type = 'remove_from_library' if self.library.movie_exists(title=entry['title'], year=entry['year']) else 'export_to_library'
+                items.append(action[action_type])
+            # add/remove show
+            if entry['type'] == 'show' and 'title' in entry_keys:
+                action_type = 'remove_from_library' if self.library.show_exists(title=entry['title']) else 'export_to_library'
+                items.append(action[action_type])
 
         # add it to the item
         li.addContextMenuItems(items)
@@ -723,11 +843,12 @@ class KodiHelper:
         level : :obj:`int`
             Kodi log level
         """
-        if level == xbmc.LOGDEBUG and self.verb_log:
-            level = xbmc.LOGNOTICE
-        if isinstance(msg, unicode):
-            msg = msg.encode('utf-8')
-        xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
+        if self.verb_log:
+            if level == xbmc.LOGDEBUG and self.verb_log:
+                level = xbmc.LOGNOTICE
+            if isinstance(msg, unicode):
+                msg = msg.encode('utf-8')
+            xbmc.log('[%s] %s' % (self.plugin, msg.__str__()), level)
 
     def get_local_string (self, string_id):
         """Returns the localized version of a string
index e7e4f5be2307863f1b6d1069b8e6847dc65552fe..8c1d841217d786f23ae27af9abcd8fe9e7b62c74 100644 (file)
@@ -4,25 +4,30 @@
 # Created on: 13.01.2017
 
 import os
-import pickle
+import shutil
+try:
+   import cPickle as pickle
+except:
+   import pickle
 from utils import noop
 
 class Library:
-    """Exports Netflix shows & movies to a local library folder (Not yet ready)"""
+    """Exports Netflix shows & movies to a local library folder"""
 
     series_label = 'shows'
+    """str: Label to identify shows"""
+
     movies_label = 'movies'
-    db_filename = 'lib.ndb'
+    """str: Label to identify movies"""
 
+    db_filename = 'lib.ndb'
+    """str: (File)Name of the store for the database dump that contains all shows/movies added to the library"""
 
-    def __init__ (self, base_url, root_folder, library_settings, log_fn=noop):
+    def __init__ (self, root_folder, library_settings, log_fn=noop):
         """Takes the instances & configuration options needed to drive the plugin
 
         Parameters
         ----------
-        base_url : :obj:`str`
-            Plugin base url
-
         root_folder : :obj:`str`
             Cookie location
 
@@ -35,44 +40,71 @@ class Library:
         log_fn : :obj:`fn`
              optional log function
         """
-        self.base_url = base_url
         self.base_data_path = root_folder
         self.enable_custom_library_folder = library_settings['enablelibraryfolder']
         self.custom_library_folder = library_settings['customlibraryfolder']
-        self.log = log_fn
         self.db_filepath = os.path.join(self.base_data_path, self.db_filename)
+        self.log = log_fn
 
         # check for local library folder & set up the paths
-        if self.enable_custom_library_folder != 'true':
-            self.movie_path = os.path.join(self.base_data_path, self.series_label)
-            self.tvshow_path = os.path.join(self.base_data_path, self.movies_label)
-        else:
-            self.movie_path = os.path.join(self.custom_library_folder, self.movies_label)
-            self.tvshow_path = os.path.join(self.custom_library_folder, self.series_label)
+        lib_path = self.base_data_path if self.enable_custom_library_folder != 'true' else self.custom_library_folder
+        self.movie_path = os.path.join(lib_path, self.movies_label)
+        self.tvshow_path = os.path.join(lib_path, self.series_label)
 
+        # check if we need to setup the base folder structure & do so if needed
         self.setup_local_netflix_library(source={
             self.movies_label: self.movie_path,
             self.series_label: self.tvshow_path
         })
 
+        # load the local db
         self.db = self._load_local_db(filename=self.db_filepath)
 
     def setup_local_netflix_library (self, source):
+        """Sets up the basic directories
+
+        Parameters
+        ----------
+        source : :obj:`dict` of :obj:`str`
+            Dicitionary with directories to be created
+        """
         for label in source:
             if not os.path.exists(source[label]):
                 os.makedirs(source[label])
 
     def write_strm_file(self, path, url):
+        """Writes the stream file that Kodi can use to integrate it into the DB
+
+        Parameters
+        ----------
+        path : :obj:`str`
+            Filepath of the file to be created
+
+        url : :obj:`str`
+            Stream url
+        """
         with open(path, 'w+') as f:
             f.write(url)
             f.close()
 
     def _load_local_db (self, filename):
+        """Loads the local db file and parses it, creates one if not existent
+
+        Parameters
+        ----------
+        filename : :obj:`str`
+            Filepath of db file
+
+        Returns
+        -------
+        :obj:`dict`
+            Parsed contents of the db file
+        """
         # if the db doesn't exist, create it
         if not os.path.isfile(filename):
             data = {self.movies_label: {}, self.series_label: {}}
             self.log('Setup local library DB')
-            self._update_local_db(filename=filename, data=data)
+            self._update_local_db(filename=filename, db=data)
             return data
 
         with open(filename) as f:
@@ -82,75 +114,216 @@ class Library:
             else:
                 return {}
 
-    def _update_local_db (self, filename, data):
+    def _update_local_db (self, filename, db):
+        """Updates the local db file with new data
+
+        Parameters
+        ----------
+        filename : :obj:`str`
+            Filepath of db file
+
+        db : :obj:`dict`
+            Database contents
+
+        Returns
+        -------
+        bool
+            Update has been successfully executed
+        """
         if not os.path.isdir(os.path.dirname(filename)):
             return False
         with open(filename, 'w') as f:
             f.truncate()
-            pickle.dump(data, f)
+            pickle.dump(db, f)
         return True
 
     def movie_exists (self, title, year):
+        """Checks if a movie is already present in the local DB
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the movie
+
+        year : :obj:`int`
+            Release year of the movie
+
+        Returns
+        -------
+        bool
+            Movie exists in DB
+        """
         movie_meta = '%s (%d)' % (title, year)
         return movie_meta in self.db[self.movies_label]
 
-    def show_exists (self, title, year):
-        show_meta = '%s (%d)' % (title, year)
+    def show_exists (self, title):
+        """Checks if a show is present in the local DB
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        Returns
+        -------
+        bool
+            Show exists in DB
+        """
+        show_meta = '%s' % (title)
         return show_meta in self.db[self.series_label]
 
-    def season_exists (self, title, year, season):
-        if self.show_exists() == False:
+    def season_exists (self, title, season):
+        """Checks if a season is present in the local DB
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        season : :obj:`int`
+            Season sequence number
+
+        Returns
+        -------
+        bool
+            Season of show exists in DB
+        """
+        if self.show_exists(title) == False:
             return False
-        show_meta = '%s (%d)' % (title, year)
-        show_entry = self.db[self.series_label][show_meta]
+        show_entry = self.db[self.series_label][title]
         return season in show_entry['seasons']
 
-    def episode_exists (self, title, year, season, episode):
-        if self.show_exists() == False:
+    def episode_exists (self, title, season, episode):
+        """Checks if an episode if a show is present in the local DB
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        season : :obj:`int`
+            Season sequence number
+
+        episode : :obj:`int`
+            Episode sequence number
+
+        Returns
+        -------
+        bool
+            Episode of show exists in DB
+        """
+        if self.show_exists(title) == False:
             return False
-        show_meta = '%s (%d)' % (title, year)
-        show_entry = self.db[self.series_label][show_meta]
+        show_entry = self.db[self.series_label][title]
         episode_entry = 'S%02dE%02d' % (season, episode)
         return episode_entry in show_entry['episodes']
 
-    def add_movie(self, title, year, video_id, pin, build_url):
+    def add_movie (self, title, alt_title, year, video_id, pin, build_url):
+        """Adds a movie to the local db, generates & persists the strm file
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        alt_title : :obj:`str`
+            Alternative title given by the user
+
+        year : :obj:`int`
+            Release year of the show
+
+        video_id : :obj:`str`
+            ID of the video to be played
+
+        pin : bool
+            Needs adult pin
+
+        build_url : :obj:`fn`
+            Function to generate the stream url
+        """
+
         movie_meta = '%s (%d)' % (title, year)
-        dirname = os.path.join(self.movie_path, movie_meta)
+        folder = alt_title
+        dirname = os.path.join(self.movie_path, folder)
         filename = os.path.join(dirname, movie_meta + '.strm')
         if os.path.exists(filename):
             return
         if not os.path.exists(dirname):
             os.makedirs(dirname)
         if self.movie_exists(title=title, year=year) == False:
-            self.db[self.movies_label][movie_meta] = True
+            self.db[self.movies_label][movie_meta] = {'alt_title': alt_title}
             self._update_local_db(filename=self.db_filepath, db=self.db)
         self.write_strm_file(path=filename, url=build_url({'action': 'play_video', 'video_id': video_id, 'pin': pin}))
 
-    def add_show(self, title, year, episodes, build_url):
-        show_meta = '%s (%d)' % (title, year)
-        show_dir = os.path.join(self.tvshow_path, show_meta)
+    def add_show (self, title, alt_title, episodes, build_url):
+        """Adds a show to the local db, generates & persists the strm files
+
+        Note: Can also used to store complete seasons or single episodes, it all depends on
+        what is present in the episodes dictionary
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        alt_title : :obj:`str`
+            Alternative title given by the user
+
+        episodes : :obj:`dict` of :obj:`dict`
+            Episodes that need to be added
+
+        build_url : :obj:`fn`
+            Function to generate the stream url
+        """
+        show_meta = '%s' % (title)
+        folder = alt_title
+        show_dir = os.path.join(self.tvshow_path, folder)
         if not os.path.exists(show_dir):
             os.makedirs(show_dir)
-        if self.show_exists(title, year) == False:
-            self.db[self.series_label][show_meta] = {'seasons': [], 'episodes': []}
-        for episode_id in episodes:
-            episode = episodes[episode_id]
-            self._add_episode(show_dir=show_dir, show_meta=show_meta, title=title, year=year, season=episode['season'], episode=episode['idx'], video_id=episode['id'], pin=episode['pin'], build_url=build_url)
+        if self.show_exists(title) == False:
+            self.db[self.series_label][show_meta] = {'seasons': [], 'episodes': [], 'alt_title': alt_title}
+        for episode in episodes:
+            self._add_episode(show_dir=show_dir, title=title, season=episode['season'], episode=episode['episode'], video_id=episode['id'], pin=episode['pin'], build_url=build_url)
         self._update_local_db(filename=self.db_filepath, db=self.db)
         return show_dir
 
-    def _add_episode(self, title, year, show_dir, show_meta, season, episode, video_id, pin, build_url):
+    def _add_episode (self, title, show_dir, season, episode, video_id, pin, build_url):
+        """Adds a single episode to the local DB, generates & persists the strm file
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        show_dir : :obj:`str`
+            Directory that holds the stream files for that show
+
+        season : :obj:`int`
+            Season sequence number
+
+        episode : :obj:`int`
+            Episode sequence number
+
+        video_id : :obj:`str`
+            ID of the video to be played
+
+        pin : bool
+            Needs adult pin
+
+        build_url : :obj:`fn`
+            Function to generate the stream url
+        """
         season = int(season)
         episode = int(episode)
 
         # add season
-        if self.season_exists(title=title, year=year, season=season) == False:
-            self.db[self.series_label][show_meta]['seasons'].append(season)
+        if self.season_exists(title=title, season=season) == False:
+            self.db[self.series_label][title]['seasons'].append(season)
 
         # add episode
         episode_meta = 'S%02dE%02d' % (season, episode)
-        if self.episode_exists(title=title, year=year, season=season, episode=episode) == False:
-            self.db[self.series_label][show_meta]['episodes'].append(episode_meta)
+        if self.episode_exists(title=title, season=season, episode=episode) == False:
+            self.db[self.series_label][title]['episodes'].append(episode_meta)
 
         # create strm file
         filename = episode_meta + '.strm'
@@ -159,36 +332,79 @@ class Library:
             return
         self.write_strm_file(path=filepath, url=build_url({'action': 'play_video', 'video_id': video_id, 'pin': pin}))
 
-    def remove_movie(self, title, year):
+    def remove_movie (self, title, year):
+        """Removes the DB entry & the strm file for the movie given
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the movie
+
+        year : :obj:`int`
+            Release year of the movie
+
+        Returns
+        -------
+        bool
+            Delete successfull
+        """
         movie_meta = '%s (%d)' % (title, year)
+        folder = self.db[self.movies_label][movie_meta]['alt_title']
         del self.db[self.movies_label][movie_meta]
         self._update_local_db(filename=self.db_filepath, db=self.db)
-        dirname = os.path.join(self.movie_path, movie_meta)
+        dirname = os.path.join(self.movie_path, folder)
         if os.path.exists(dirname):
-            os.rmtree(dirname)
+            shutil.rmtree(dirname)
             return True
         return False
 
-    def remove_show(self, title, year):
-        show_meta = '%s (%d)' % (title, year)
-        del self.db[self.series_label][show_meta]
+    def remove_show (self, title):
+        """Removes the DB entry & the strm files for the show given
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        Returns
+        -------
+        bool
+            Delete successfull
+        """
+        folder = self.db[self.series_label][title]['alt_title']
+        del self.db[self.series_label][title]
         self._update_local_db(filename=self.db_filepath, db=self.db)
-        show_dir = os.path.join(self.tvshow_path, show_meta)
+        show_dir = os.path.join(self.tvshow_path, folder)
         if os.path.exists(show_dir):
-            os.rmtree(show_dir)
+            shutil.rmtree(show_dir)
             return True
         return False
 
-    def remove_season(self, title, year, season):
+    def remove_season (self, title, season):
+        """Removes the DB entry & the strm files for a season of a show given
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        season : :obj:`int`
+            Season sequence number
+
+        Returns
+        -------
+        bool
+            Delete successfull
+        """
         season = int(season)
         season_list = []
         episodes_list = []
-        show_meta = '%s (%d)' % (title, year)
+        show_meta = '%s' % (title)
         for season_entry in self.db[self.series_label][show_meta]['seasons']:
             if season_entry != season:
                 season_list.append(season_entry)
         self.db[self.series_label][show_meta]['seasons'] = season_list
-        show_dir = os.path.join(self.tvshow_path, show_meta)
+        show_dir = os.path.join(self.tvshow_path, self.db[self.series_label][show_meta]['alt_title'])
         if os.path.exists(show_dir):
             show_files = [f for f in os.listdir(show_dir) if os.path.isfile(os.path.join(show_dir, f))]
             for filename in show_files:
@@ -200,11 +416,29 @@ class Library:
         self._update_local_db(filename=self.db_filepath, db=self.db)
         return True
 
-    def remove_episode(self, title, year, season, episode):
+    def remove_episode (self, title, season, episode):
+        """Removes the DB entry & the strm files for an episode of a show given
+
+        Parameters
+        ----------
+        title : :obj:`str`
+            Title of the show
+
+        season : :obj:`int`
+            Season sequence number
+
+        episode : :obj:`int`
+            Episode sequence number
+
+        Returns
+        -------
+        bool
+            Delete successfull
+        """
         episodes_list = []
-        show_meta = '%s (%d)' % (title, year)
+        show_meta = '%s' % (title)
         episode_meta = 'S%02dE%02d' % (season, episode)
-        show_dir = os.path.join(self.tvshow_path, show_meta)
+        show_dir = os.path.join(self.tvshow_path, self.db[self.series_label][show_meta]['alt_title'])
         if os.path.exists(os.path.join(show_dir, episode_meta + '.strm')):
             os.remove(os.path.join(show_dir, episode_meta + '.strm'))
         for episode_entry in self.db[self.series_label][show_meta]['episodes']:
index c1e9397278dcd0fc97f22e8d839014c8ca281cb9..e9346453580acdcf48976f83f2571821c0215f15 100644 (file)
@@ -55,9 +55,10 @@ class Navigation:
 
         # check login & try to relogin if necessary
         account = self.kodi_helper.get_credentials()
-        if self.netflix_session.is_logged_in(account=account) != True:
-            if self.establish_session(account=account) != True:
-                return self.kodi_helper.show_login_failed_notification()
+        if account['email'] != '' and account['password'] != '':
+            if self.netflix_session.is_logged_in(account=account) != True:
+                if self.establish_session(account=account) != True:
+                    return self.kodi_helper.show_login_failed_notification()
 
         # check if we need to execute any actions before the actual routing
         # gives back a dict of options routes might need
@@ -86,10 +87,19 @@ class Navigation:
             return self.rate_on_netflix(video_id=params['id'])
         elif params['action'] == 'remove_from_list':
             # removes a title from the users list on Netflix
+            self.kodi_helper.invalidate_memcache()
             return self.remove_from_list(video_id=params['id'])
         elif params['action'] == 'add_to_list':
             # adds a title to the users list on Netflix
+            self.kodi_helper.invalidate_memcache()
             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=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
+            return self.remove_from_library(video_id=params['id'])
         elif params['action'] == 'user-items' and params['type'] != 'search':
             # display the lists (recommendations, genres, etc.)
             return self.show_user_list(type=params['type'])
@@ -127,6 +137,7 @@ class Navigation:
         esn = self.netflix_session.esn
         return self.kodi_helper.play_item(esn=esn, video_id=video_id, start_offset=start_offset)
 
+    @log
     def show_search_results (self, term):
         """Display a list of search results
 
@@ -141,7 +152,7 @@ class Navigation:
             If no results are available
         """
         has_search_results = False
-        search_results_raw = self.netflix_session.fetch_search_results(term=term)
+        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
@@ -190,12 +201,17 @@ class Navigation:
         season_id : :obj:`str`
             ID of the season episodes should be displayed for
         """
-        raw_episode_list = self.netflix_session.fetch_episodes_by_season(season_id=season_id)
-        # check for any errors
-        if self._is_dirty_response(response=raw_episode_list):
-            return False
-        # parse the raw Netflix data
-        episode_list = self.netflix_session.parse_episodes_by_season(response_data=raw_episode_list)
+        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:
+            raw_episode_list = self.netflix_session.fetch_episodes_by_season(season_id=season_id)
+            # check for any errors
+            if self._is_dirty_response(response=raw_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)
         episodes_sorted = []
@@ -219,15 +235,20 @@ class Navigation:
         bool
             If no seasons are available
         """
-        season_list_raw = self.netflix_session.fetch_seasons_for_show(id=show_id);
-        # check for any errors
-        if self._is_dirty_response(response=season_list_raw):
-            return False
-        # check if we have sesons, announced shows that are not available yet have none
-        if 'seasons' not in season_list_raw['value']:
-            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)
+        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_raw = self.netflix_session.fetch_seasons_for_show(id=show_id);
+            # check for any errors
+            if self._is_dirty_response(response=season_list_raw):
+                return False
+            # check if we have sesons, announced shows that are not available yet have none
+            if 'seasons' not in season_list_raw['value']:
+                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 = []
         for season_id in season_list:
@@ -246,33 +267,47 @@ class Navigation:
         type : :obj:`str`
             None or 'queue' f.e. when it´s a special video lists
         """
-        raw_video_list = self.netflix_session.fetch_video_list(list_id=video_list_id)
-        # check for any errors
-        if self._is_dirty_response(response=raw_video_list):
-            return False
-        # parse the video list ids
-        video_list = self.netflix_session.parse_video_list(response_data=raw_video_list)
+        if self.kodi_helper.has_cached_item(cache_id=type):
+            video_list = self.kodi_helper.get_cached_item(cache_id=type)
+        else:
+            raw_video_list = self.netflix_session.fetch_video_list(list_id=video_list_id)
+            # check for any errors
+            if self._is_dirty_response(response=raw_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)
+                self.kodi_helper.add_cached_item(cache_id=type, 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)
 
     def show_video_lists (self):
         """List the users video lists (recommendations, my list, etc.)"""
-        # fetch video lists
-        raw_video_list_ids = self.netflix_session.fetch_video_list_ids()
-        # check for any errors
-        if self._is_dirty_response(response=raw_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)
+        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)
+        else:
+            # fetch video lists
+            raw_video_list_ids = self.netflix_session.fetch_video_list_ids()
+            # check for any errors
+            if self._is_dirty_response(response=raw_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']
         # define where to route the user
         actions = {'recommendations': 'user-items', 'genres': 'user-items', 'search': 'user-items', 'default': 'video_list'}
         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)
 
+    @log
     def show_profiles (self):
         """List the profiles for the active account"""
-        self.netflix_session.refresh_session_data(account=self.kodi_helper.get_credentials())
+        credentials = self.kodi_helper.get_credentials()
+        self.netflix_session.refresh_session_data(account=credentials)
         profiles = self.netflix_session.profiles
         return self.kodi_helper.build_profiles_listing(profiles=profiles, action='video_lists', build_url=self.build_url)
 
@@ -312,6 +347,56 @@ class Navigation:
         self.netflix_session.add_to_list(video_id=video_id)
         return self.kodi_helper.refresh()
 
+    @log
+    def export_to_library (self, video_id, alt_title):
+        """Adds an item to the local library
+
+        Parameters
+        ----------
+        video_id : :obj:`str`
+            ID of the movie or show
+
+        alt_title : :obj:`str`
+            Alternative title (for the folder written to disc)
+        """
+        metadata = self.netflix_session.fetch_metadata(id=video_id)
+        # check for any errors
+        if self._is_dirty_response(response=metadata):
+            return False
+        video = metadata['video']
+
+        if video['type'] == 'movie':
+            self.library.add_movie(title=video['title'], alt_title=alt_title, year=video['year'], video_id=video_id, pin=video['requiresPin'], build_url=self.build_url)
+        if video['type'] == 'show':
+            episodes = []
+            for season in video['seasons']:
+                for episode in season['episodes']:
+                    episodes.append({'season': season['seq'], 'episode': episode['seq'], 'id': episode['id'], 'pin': episode['requiresAdultVerification']})
+
+            self.library.add_show(title=video['title'], alt_title=alt_title, episodes=episodes, build_url=self.build_url)
+        return self.kodi_helper.refresh()
+
+    @log
+    def remove_from_library (self, video_id, season=None, episode=None):
+        """Removes an item from the local library
+
+        Parameters
+        ----------
+        video_id : :obj:`str`
+            ID of the movie or show
+        """
+        metadata = self.netflix_session.fetch_metadata(id=video_id)
+        # check for any errors
+        if self._is_dirty_response(response=metadata):
+            return False
+        video = metadata['video']
+
+        if video['type'] == 'movie':
+            self.library.remove_movie(title=video['title'], year=video['year'])
+        if video['type'] == 'show':
+            self.library.remove_show(title=video['title'])
+        return self.kodi_helper.refresh()
+
     @log
     def establish_session(self, account):
         """Checks if we have an cookie with an active sessions, otherwise tries to login the user
@@ -326,10 +411,7 @@ class Navigation:
         bool
             If we don't have an active session & the user couldn't be logged in
         """
-        if self.netflix_session.is_logged_in(account=account):
-            return True
-        else:
-            return self.netflix_session.login(account=account)
+        return True if self.netflix_session.is_logged_in(account=account) else self.netflix_session.login(account=account)
 
     @log
     def before_routing_action (self, params):
@@ -355,15 +437,18 @@ class Navigation:
         if credentials['email'] == '':
             email = self.kodi_helper.show_email_dialog()
             self.kodi_helper.set_setting(key='email', value=email)
+            credentials['email'] = email
         if credentials['password'] == '':
             password = self.kodi_helper.show_password_dialog()
             self.kodi_helper.set_setting(key='password', value=password)
+            credentials['password'] = password
         # persist & load main menu selection
         if 'type' in params:
             self.kodi_helper.set_main_menu_selection(type=params['type'])
         options['main_menu_selection'] = self.kodi_helper.get_main_menu_selection()
         # 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)
         # check login, in case of main menu
         if 'action' not in params:
@@ -453,7 +538,9 @@ class Navigation:
             if self._is_expired_session(response=response):
                 if self.establish_session(account=self.kodi_helper.get_credentials()):
                     return True
-            self.log(msg='[ERROR]: ' + response['message'] + '::' + str(response['code']))
+            message = response['message'] if 'message' in response else ''
+            code = response['code'] if 'code' in response else ''
+            self.log(msg='[ERROR]: ' + message + '::' + str(code))
             return True
         return False
 
index b2416c298a6f5a43cad179188db043563c2ecb29..2ba0cbb9bd4722dcfcf3b7e071e509ea60a0dd17 100644 (file)
@@ -10,15 +10,18 @@ import time
 import urllib
 import json
 import requests
-import pickle
-from BeautifulSoup import BeautifulSoup
+try:
+   import cPickle as pickle
+except:
+   import pickle
+from bs4 import BeautifulSoup
 from utils import strip_tags
 from utils import noop
 
 class NetflixSession:
     """Helps with login/session management of Netflix users & API data fetching"""
 
-    base_url = 'https://www.netflix.com/'
+    base_url = 'https://www.netflix.com'
     """str: Secure Netflix url"""
 
     urls = {
@@ -86,7 +89,7 @@ class NetflixSession:
     esn = ''
     """str: Widevine esn, something like: NFCDCH-MC-D7D6F54LOPY8J416T72MQXX3RD20ME"""
 
-    def __init__(self, cookie_path, data_path, log_fn=noop):
+    def __init__(self, cookie_path, data_path, verify_ssl=True, log_fn=noop):
         """Stores the cookie path for later use & instanciates a requests
            session with a proper user agent & stored cookies/data if available
 
@@ -103,6 +106,7 @@ class NetflixSession:
         """
         self.cookie_path = cookie_path
         self.data_path = data_path
+        self.verify_ssl = verify_ssl
         self.log = log_fn
 
         # start session, fake chrome (so that we get a proper widevine esn) & enable gzip
@@ -128,7 +132,7 @@ class NetflixSession:
                 value from the form field
         """
         login_input_fields = {}
-        login_inputs = form_soup.findAll('input')
+        login_inputs = form_soup.find_all('input')
         # gather all form fields, set an empty string as the default value
         for item in login_inputs:
             keys = dict(item.attrs).keys()
@@ -166,7 +170,7 @@ class NetflixSession:
                 List of all the serialized data pulled out of the pagws <script/> tags
         """
         inline_data = [];
-        data_scripts = page_soup.findAll('script', attrs={'src': None});
+        data_scripts = page_soup.find_all('script', attrs={'src': None});
         for script in data_scripts:
             # ugly part: try to parse the data & don't care about errors (as they will be some)
             try:
@@ -374,7 +378,7 @@ class NetflixSession:
             return False
         if self._load_data(filename=self.data_path + '_' + account_hash) == False:
             # load the profiles page (to verify the user)
-            response = self.session.get(self._get_document_url_for(component='profiles'))
+            response = self.session.get(self._get_document_url_for(component='profiles'), verify=self.verify_ssl)
 
             # parse out the needed inline information
             page_soup = BeautifulSoup(response.text)
@@ -418,7 +422,7 @@ class NetflixSession:
         bool
             User could be logged in or not
         """
-        response = self.session.get(self._get_document_url_for(component='login'))
+        response = self.session.get(self._get_document_url_for(component='login'), verify=self.verify_ssl)
         if response.status_code != 200:
             return False;
 
@@ -433,7 +437,7 @@ class NetflixSession:
         login_payload['password'] = account['password']
 
         # perform the login
-        login_response = self.session.post(self._get_document_url_for(component='login'), data=login_payload)
+        login_response = self.session.post(self._get_document_url_for(component='login'), data=login_payload, verify=self.verify_ssl)
         login_soup = BeautifulSoup(login_response.text)
 
         # we know that the login was successfull if we find an HTML element with the class of 'profile-name'
@@ -471,12 +475,12 @@ class NetflixSession:
             'authURL': self.user_data['authURL']
         }
 
-        response = self.session.get(self._get_api_url_for(component='switch_profiles'), params=payload);
+        response = self.session.get(self._get_api_url_for(component='switch_profiles'), params=payload, verify=self.verify_ssl);
         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(self._get_document_url_for(component='browse'))
+        browse_response = self.session.get(self._get_document_url_for(component='browse'), verify=self.verify_ssl)
         browse_soup = BeautifulSoup(browse_response.text)
         self._parse_page_contents(page_soup=browse_soup)
         account_hash = self._generate_account_hash(account=account)
@@ -506,7 +510,7 @@ class NetflixSession:
             'authURL': self.user_data['authURL']
         }
         url = self._get_api_url_for(component='adult_pin')
-        response = self.session.get(url, params=payload);
+        response = self.session.get(url, params=payload, verify=self.verify_ssl);
         pin_response = self._process_response(response=response, component=url)
         keys = pin_response.keys()
         if 'success' in keys:
@@ -585,7 +589,7 @@ class NetflixSession:
             'authURL': self.user_data['authURL']
         })
 
-        response = self.session.post(self._get_api_url_for(component='set_video_rating'), params=params, headers=headers, data=payload)
+        response = self.session.post(self._get_api_url_for(component='set_video_rating'), params=params, headers=headers, data=payload, verify=self.verify_ssl)
         return response.status_code == 200
 
     def parse_video_list_ids (self, response_data):
@@ -1460,7 +1464,7 @@ class NetflixSession:
         :obj:`BeautifulSoup`
             Instance of an BeautifulSoup document containing the complete page contents
         """
-        response = self.session.get(self._get_document_url_for(component='browse'))
+        response = self.session.get(self._get_document_url_for(component='browse'), verify=self.verify_ssl)
         return BeautifulSoup(response.text)
 
     def fetch_video_list_ids (self, list_from=0, list_to=50):
@@ -1488,7 +1492,7 @@ class NetflixSession:
             'authURL': self.user_data['authURL']
         }
         url = self._get_api_url_for(component='video_list_ids')
-        response = self.session.get(url, params=payload);
+        response = self.session.get(url, params=payload, verify=self.verify_ssl);
         return self._process_response(response=response, component=url)
 
     def fetch_search_results (self, search_str, list_from=0, list_to=48):
@@ -1614,7 +1618,7 @@ class NetflixSession:
             '_': int(time.time())
         }
         url = self._get_api_url_for(component='metadata')
-        response = self.session.get(url, params=payload);
+        response = self.session.get(url, params=payload, verify=self.verify_ssl);
         return self._process_response(response=response, component=url)
 
     def fetch_show_information (self, id, type):
@@ -1724,8 +1728,7 @@ class NetflixSession:
             Dict containing an email, country & a password property
         """
         # load the profiles page (to verify the user)
-        response = self.session.get(self._get_document_url_for(component='profiles'))
-
+        response = self.session.get(self._get_document_url_for(component='profiles'), verify=self.verify_ssl)
         # parse out the needed inline information
         page_soup = BeautifulSoup(response.text)
         page_data = self.extract_inline_netflix_page_data(page_soup=page_soup)
@@ -1762,7 +1765,7 @@ class NetflixSession:
             'model': self.user_data['gpsModel']
         }
 
-        return self.session.post(self._get_api_url_for(component='shakti'), params=params, headers=headers, data=data)
+        return self.session.post(self._get_api_url_for(component='shakti'), params=params, headers=headers, data=data, verify=self.verify_ssl)
 
     def _is_size_key (self, key):
         """Tiny helper that checks if a given key is called $size or size, as we need to check this often
@@ -1869,7 +1872,7 @@ class NetflixSession:
             'authURL': self.user_data['authURL']
         })
 
-        response = self.session.post(self._get_api_url_for(component='update_my_list'), headers=headers, data=payload)
+        response = self.session.post(self._get_api_url_for(component='update_my_list'), headers=headers, data=payload, verify=self.verify_ssl)
         return response.status_code == 200
 
     def _save_data(self, filename):
index 3f9f9c0f2c857e30625fdabb495679b5dc65d82e..68779c0142cd3450a7e367e4c0b7f8cb61a1052f 100644 (file)
@@ -11,6 +11,8 @@
   </category>
   <category label="30023">
     <setting id="ssl_verification" type="bool" label="30024" default="true"/>
+    <setting id="msl_service_port" value="8000" visible="false"/>
+    <setting id="msl_service_certificate" visible="false" value="Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eAGuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRITgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95OEtzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vfS6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0nu39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlGGCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqVDXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nAovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ0fFdCmw="/>
   </category>
   <category label="30015">
     <setting id="logging" type="bool" label="30016" default="false"/>