feat(core): Adds library export for movies & shows, fixes for search, fixes for displ...
[plugin.video.netflix.git] / resources / lib / Library.py
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']: