Repository: Unmanic/unmanic (language: python)

Issue:
Ensure library names are unique

Plugin settings to be configured per library
This is only relevant to plugins that are used in the post-processor, worker or file tester.
Other plugins like the data panels will only be configurable via the main plugin settings.


Discussion / hints:

Fix patch:
diff --git a/docker/Dockerfile b/docker/Dockerfile
index de023f0e..3634ab2b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -18,7 +18,7 @@ RUN \
         && apt-get update \
         && apt-get install -y \
             libexpat1=2.2.9-1build1 \
-            libgl1-mesa-dri=21.0.3-0ubuntu0.3~20.04.5 \
+            libgl1-mesa-dri=21.2.6-0ubuntu0.1~20.04.1 \
             libglib2.0-0=2.64.6-1~ubuntu20.04.4 \
             libgomp1=10.3.0-1ubuntu1~20.04 \
             libharfbuzz0b=2.6.4-1ubuntu4 \
@@ -59,7 +59,7 @@ RUN \
             && \
             echo "**** Install MESA Media Drivers for AMD VAAPI ****" \
                 && apt-get install -y \
-                    mesa-va-drivers=21.0.3-0ubuntu0.3~20.04.5 \
+                    mesa-va-drivers=21.2.6-0ubuntu0.1~20.04.1 \
             && \
             echo "**** Remove build packages ****" \
                 && apt-get remove -y \
diff --git a/unmanic/config.py b/unmanic/config.py
index 7da12fe0..85b9a699 100644
--- a/unmanic/config.py
+++ b/unmanic/config.py
@@ -69,7 +69,6 @@ def __init__(self, config_path=None, **kwargs):
         self.follow_symlinks = True
         self.concurrent_file_testers = 2
         self.run_full_scan_on_start = False
-        self.enable_inotify = False
         self.clear_pending_tasks_on_restart = True
 
         # Worker settings
@@ -85,6 +84,12 @@ def __init__(self, config_path=None, **kwargs):
         # Import env variables and override all previous settings.
         self.__import_settings_from_env()
 
+        # Import Unmanic path settings from command params
+        if kwargs.get('unmanic_path'):
+            self.set_config_item('config_path', os.path.join(kwargs.get('unmanic_path'), 'config'), save_settings=False)
+            self.set_config_item('plugins_path', os.path.join(kwargs.get('unmanic_path'), 'plugins'), save_settings=False)
+            self.set_config_item('userdata_path', os.path.join(kwargs.get('unmanic_path'), 'userdata'), save_settings=False)
+
         # Finally, re-read config from file and override all previous settings.
         self.__import_settings_from_file(config_path)
 
@@ -92,11 +97,7 @@ def __init__(self, config_path=None, **kwargs):
         if config_path:
             self.set_config_item('config_path', config_path, save_settings=False)
 
-        if kwargs.get('unmanic_path'):
-            self.set_config_item('config_path', os.path.join(kwargs.get('unmanic_path'), 'config'), save_settings=False)
-            self.set_config_item('plugins_path', os.path.join(kwargs.get('unmanic_path'), 'plugins'), save_settings=False)
-            self.set_config_item('userdata_path', os.path.join(kwargs.get('unmanic_path'), 'userdata'), save_settings=False)
-
+        # Overwrite all other settings passed from command params
         if kwargs.get('port'):
             self.set_config_item('ui_port', kwargs.get('port'), save_settings=False)
 
@@ -330,14 +331,6 @@ def set_debugging(self, value):
             unmanic_logging.disable_debugging()
         self.debugging = value
 
-    def get_enable_inotify(self):
-        """
-        Get setting - enable_inotify
-
-        :return:
-        """
-        return self.enable_inotify
-
     def get_library_path(self):
         """
         Get setting - library_path
diff --git a/unmanic/libs/eventmonitor.py b/unmanic/libs/eventmonitor.py
index 06231250..c2b5b71e 100644
--- a/unmanic/libs/eventmonitor.py
+++ b/unmanic/libs/eventmonitor.py
@@ -35,6 +35,7 @@
 import time
 
 from unmanic import config
+from unmanic.libs.library import Library
 from unmanic.libs.plugins import PluginsHandler
 
 try:
@@ -76,9 +77,10 @@ class EventHandler(FileSystemEventHandler):
     From this, the only event we really need to monitor is the "created" and "closed" events.
     """
 
-    def __init__(self, files_to_test):
+    def __init__(self, files_to_test, library_id):
         self.name = "EventProcessor"
         self.files_to_test = files_to_test
+        self.library_id = library_id
         self.logger = None
         self.abort_flag = threading.Event()
         self.abort_flag.clear()
@@ -98,7 +100,10 @@ def on_any_event(self, event):
                 self._log("Detected event is for a directory. Ignoring...", level="debug")
             else:
                 self._log("Detected '{}' event on file path '{}'".format(event.event_type, event.src_path))
-                self.files_to_test.put(event.src_path)
+                self.files_to_test.put({
+                    'src_path':   event.src_path,
+                    'library_id': self.library_id,
+                })
 
 
 class EventMonitorManager(threading.Thread):
@@ -125,6 +130,7 @@ def __init__(self, data_queues):
         self.abort_flag.clear()
 
         self.event_observer_thread = None
+        self.event_observer_threads = []
 
     def _log(self, message, message2='', level="info"):
         if not self.logger:
@@ -142,17 +148,27 @@ def run(self):
         self._log("Starting EventMonitorManager loop")
         while not self.abort_flag.is_set():
 
-            # Ensure all enabled plugins are compatible before starting the event monitor
-            if not self.all_plugins_are_compatible():
+            if not self.system_configuration_is_valid():
                 time.sleep(2)
                 continue
 
             if not self.files_to_test.empty():
-                self.manage_event_queue(self.files_to_test.get())
+                item = self.files_to_test.get()
+                pathname = item.get('src_path')
+                library_id = item.get('library_id')
+                self.manage_event_queue(pathname, library_id)
                 continue
 
-            # Check settings to ensure the event monitor should be enabled...
-            if self.settings.get_enable_inotify():
+            # Check if monitor is enabled for at least one library
+            enable_inotify = False
+            for lib_info in Library.get_all_libraries():
+                library = Library(lib_info['id'])
+                # Check if library scanner is enabled on any library
+                if library.get_enable_inotify():
+                    enable_inotify = True
+
+            # If at least library has the monitor enabled, then start it. Otherwise stop the monitor process
+            if enable_inotify:
                 # If enabled, ensure it is running and start it if it is not
                 if not self.event_observer_thread:
                     self.start_event_processor()
@@ -166,13 +182,17 @@ def run(self):
         self.stop_event_processor()
         self._log("Leaving EventMonitorManager loop...")
 
-    def all_plugins_are_compatible(self):
-        """Ensure all plugins are compatible before running"""
+    def system_configuration_is_valid(self):
+        """
+        Check and ensure the system configuration is correct for running
+
+        :return:
+        """
         valid = True
         plugin_handler = PluginsHandler()
         if plugin_handler.get_incompatible_enabled_plugins(self.data_queues.get('frontend_messages')):
             valid = False
-        if not plugin_handler.within_enabled_plugin_limits(self.data_queues.get('frontend_messages')):
+        if not Library.within_library_count_limits(self.data_queues.get('frontend_messages')):
             valid = False
         return valid
 
@@ -183,17 +203,24 @@ def start_event_processor(self):
         :return:
         """
         if not self.event_observer_thread:
-            library_path = self.settings.get_library_path()
-            if not os.path.exists(library_path):
-                time.sleep(.1)
-                return
-            self._log("EventMonitorManager spawning EventProcessor thread...")
-            event_handler = EventHandler(self.files_to_test)
-
+            monitoring_path = False
             self.event_observer_thread = Observer()
-            self.event_observer_thread.schedule(event_handler, self.settings.get_library_path(), recursive=True)
-
-            self.event_observer_thread.start()
+            for lib_info in Library.get_all_libraries():
+                library = Library(lib_info['id'])
+                # Check if library scanner is enabled on any library
+                if library.get_enable_inotify():
+                    library_path = library.get_path()
+                    if not os.path.exists(library_path):
+                        time.sleep(.1)
+                        continue
+                    self._log("Adding library path to monitor '{}'".format(library_path))
+                    event_handler = EventHandler(self.files_to_test, library.get_id())
+                    self.event_observer_thread.schedule(event_handler, library_path, recursive=True)
+                    monitoring_path = True
+            # Only start observer if a path was added to be monitored
+            if monitoring_path:
+                self._log("EventMonitorManager spawning EventProcessor thread...")
+                self.event_observer_thread.start()
         else:
             self._log("Given signal to start the EventProcessor thread, but it is already running....")
 
@@ -217,7 +244,7 @@ def stop_event_processor(self):
 
         self.event_observer_thread = None
 
-    def manage_event_queue(self, pathname):
+    def manage_event_queue(self, pathname, library_id):
         """
         Manage all monitored events
 
@@ -225,11 +252,12 @@ def manage_event_queue(self, pathname):
         This avoids a file being added twice on 2 events.
 
         :param pathname:
+        :param library_id:
         :return:
         """
         # Test file to be added to task list. Add it if required
         try:
-            file_test = FileTest()
+            file_test = FileTest(library_id)
             result, issues = file_test.should_file_be_added_to_task_list(pathname)
             # Log any error messages
             for issue in issues:
@@ -239,17 +267,21 @@ def manage_event_queue(self, pathname):
                     self._log(issue)
             # If file needs to be added, then add it
             if result:
-                self.__add_path_to_queue(pathname)
+                self.__add_path_to_queue(pathname, library_id)
         except UnicodeEncodeError:
             self._log("File contains Unicode characters that cannot be processed. Ignoring.", level="warning")
         except Exception as e:
             self._log("Exception testing file path in {}. Ignoring.".format(self.name), message2=str(e), level="exception")
 
-    def __add_path_to_queue(self, pathname):
+    def __add_path_to_queue(self, pathname, library_id):
         """
         Add a given path to the pending task queue
 
         :param pathname:
+        :param library_id:
         :return:
         """
-        self.data_queues.get('inotifytasks').put(pathname)
+        self.data_queues.get('inotifytasks').put({
+            'pathname':   pathname,
+            'library_id': library_id,
+        })
diff --git a/unmanic/libs/foreman.py b/unmanic/libs/foreman.py
index fb42b46a..c2bd4f53 100644
--- a/unmanic/libs/foreman.py
+++ b/unmanic/libs/foreman.py
@@ -37,6 +37,7 @@
 from datetime import datetime
 
 from unmanic.libs import common, installation_link
+from unmanic.libs.library import Library
 from unmanic.libs.plugins import PluginsHandler
 from unmanic.libs.workers import Worker
 
@@ -99,8 +100,26 @@ def save_plugin_config(self, settings=None, settings_hash=None):
         self._log('Updated plugin config', message2=self.plugin_config, level='debug')
 
     def get_current_plugin_configuration(self):
-        plugin_handler = PluginsHandler()
-        all_plugin_settings = plugin_handler.get_settings_of_all_enabled_plugins()
+        # Fetch all libraries
+        all_plugin_settings = {}
+        for library in Library.get_all_libraries():
+            library_config = Library(library.get('id'))
+            # Get list of enabled plugins with their settings
+            enabled_plugins = []
+            for enabled_plugin in library_config.get_enabled_plugins(include_settings=True):
+                enabled_plugins.append({
+                    'plugin_id': enabled_plugin.get('plugin_id'),
+                    'settings':  enabled_plugin.get('settings'),
+                })
+
+            # Get the plugin flow
+            plugin_flow = library_config.get_plugin_flow()
+
+            # Append this library's plugin config and flow the the dictionary
+            all_plugin_settings[library.get('id')] = {
+                'enabled_plugins': enabled_plugins,
+                'plugin_flow':     plugin_flow,
+            }
         return all_plugin_settings
 
     def plugin_configuration_changed(self):
@@ -123,8 +142,6 @@ def validate_worker_config(self):
         plugin_handler = PluginsHandler()
         if plugin_handler.get_incompatible_enabled_plugins(frontend_messages):
             valid = False
-        if not plugin_handler.within_enabled_plugin_limits(frontend_messages):
-            valid = False
         if not self.links.within_enabled_link_limits(frontend_messages):
             valid = False
 
@@ -144,6 +161,10 @@ def validate_worker_config(self):
             )
             valid = False
 
+        # Ensure library config is within limits
+        if not Library.within_library_count_limits(frontend_messages):
+            valid = False
+
         return valid
 
     def run_task(self, time_now, task, worker_count=1):
diff --git a/unmanic/libs/library.py b/unmanic/libs/library.py
new file mode 100644
index 00000000..aa1ad690
--- /dev/null
+++ b/unmanic/libs/library.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+    unmanic.library.py
+
+    Written by:               Josh.5 <jsunnex@gmail.com>
+    Date:                     06 Feb 2022, (12:11 PM)
+
+    Copyright:
+           Copyright (C) Josh Sunnex - All Rights Reserved
+
+           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.
+
+"""
+from unmanic.config import Config
+from unmanic.libs.unmodels import EnabledPlugins, Libraries, LibraryPluginFlow, Plugins, Tasks
+
+
+class Library(object):
+    """
+    Library
+
+    Contains all data pertaining to a library
+
+    """
+
+    def __init__(self, library_id: int):
+        # Ensure library ID is not 0
+        if library_id < 1:
+            raise Exception("Library ID cannot be less than 1")
+        self.model = Libraries.get_or_none(id=library_id)
+        if not self.model:
+            raise Exception("Unable to fetch library with ID {}".format(library_id))
+
+    @staticmethod
+    def get_all_libraries():
+        """
+        Return a list of all libraries
+
+        :return:
+        """
+        # Fetch default library path from
+        from unmanic.config import Config
+        default_library_path = Config().get_library_path()
+
+        # Fetch all libraries from DB
+        configured_libraries = Libraries.select()
+
+        # Ensure that at least the default path was added.
+        # If the libraries path is empty, then we should add the default path
+        if not configured_libraries:
+            default_library = {
+                'id':   1,
+                'name': 'Default',
+                'path': default_library_path,
+            }
+            Libraries.create(**default_library)
+            return [default_library]
+
+        # Loop over results
+        libraries = []
+        for lib in configured_libraries:
+            # Always update the default library path
+            if lib.id == 1 and lib.path != default_library_path:
+                lib.path = default_library_path
+                lib.save()
+            libraries.append({
+                'id':   lib.id,
+                'name': lib.name,
+                'path': lib.path,
+            })
+
+        # Return the list of libraries
+        return libraries
+
+    @staticmethod
+    def within_library_count_limits(frontend_messages=None):
+        # Fetch level from session
+        from unmanic.libs.session import Session
+        s = Session()
+        s.register_unmanic()
+        if s.level > 1:
+            return True
+
+        # Fetch all enabled plugins
+        library_count = Libraries.select().count()
+
+        def add_frontend_message():
+            # If the frontend messages queue was included in request, append a message
+            if frontend_messages:
+                frontend_messages.put(
+                    {
+                        'id':      'libraryEnabledLimits',
+                        'type':    'error',
+                        'code':    'libraryEnabledLimits',
+                        'message': '',
+                        'timeout': 0
+                    }
+                )
+
+        # Ensure enabled plugins are within limits
+        # Function was returned above if the user was logged in and able to use infinite
+        if library_count > s.library_count:
+            add_frontend_message()
+            return False
+        return True
+
+    @staticmethod
+    def create(data: dict):
+        """
+        Create a new library
+
+        :param data:
+        :return:
+        """
+        # Ensure ID is removed from data for a create
+        if 'id' in data:
+            del data['id']
+        new_library = Libraries.create(**data)
+        return Library(new_library.id)
+
+    def __remove_enabled_plugins(self):
+        """
+        Remove all enabled plugins
+
+        :return:
+        """
+        query = EnabledPlugins.delete()
+        query = query.where(EnabledPlugins.library_id == self.model.id)
+        return query.execute()
+
+    def __trim_plugin_flow(self, plugin_ids: list):
+        """
+        Trim the plugin flow removing entries not in the given plugin ids list
+
+        :param plugin_ids:
+        :return:
+        """
+        query = LibraryPluginFlow.delete()
+        query = query.where((LibraryPluginFlow.library_id == self.model.id) & (LibraryPluginFlow.plugin_id.not_in(plugin_ids)))
+        return query.execute()
+
+    def __remove_associated_tasks(self):
+        """
+        Remove all tasks associated with a library
+
+        :return:
+        """
+        Tasks.delete().where(Tasks.library_id == self.model.id).execute()
+
+    def get_id(self):
+        return self.model.id
+
+    def get_name(self):
+        return self.model.name
+
+    def set_name(self, value):
+        self.model.name = value
+
+    def get_path(self):
+        return self.model.path
+
+    def set_path(self, value):
+        self.model.path = value
+
+    def get_enable_scanner(self):
+        return self.model.enable_scanner
+
+    def set_enable_scanner(self, value):
+        self.model.enable_scanner = value
+
+    def get_enable_inotify(self):
+        return self.model.enable_inotify
+
+    def set_enable_inotify(self, value):
+        self.model.enable_inotify = value
+
+    def get_enabled_plugins(self, include_settings=False):
+        """
+        Get all enabled plugins for this library
+
+        :return:
+        """
+        # Fetch enabled plugins for this library
+        query = self.model.enabled_plugins.select(Plugins, EnabledPlugins.library_id)
+        query = query.join(Plugins, join_type='LEFT OUTER JOIN', on=(EnabledPlugins.plugin_id == Plugins.id))
+        query = query.order_by(Plugins.name)
+
+        from unmanic.libs.unplugins import PluginExecutor
+        plugin_executor = PluginExecutor()
+
+        # Extract required data
+        enabled_plugins = []
+        for enabled_plugin in query.dicts():
+            # Check if plugin is able to be configured
+            has_config = False
+            plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(enabled_plugin.get('plugin_id'),
+                                                                                        library_id=self.model.id)
+            if plugin_settings:
+                has_config = True
+            # Add plugin to list of enabled plugins
+            item = {
+                'plugin_id':   enabled_plugin.get('plugin_id'),
+                'name':        enabled_plugin.get('name'),
+                'description': enabled_plugin.get('description'),
+                'icon':        enabled_plugin.get('icon'),
+                'has_config':  has_config,
+            }
+            if include_settings:
+                item['settings'] = plugin_settings
+            enabled_plugins.append(item)
+
+        return enabled_plugins
+
+    def get_plugin_flow(self):
+        """
+        Fetch the plugin flow for a library
+
+        :return:
+        """
+        plugin_flow = {}
+        from unmanic.libs.plugins import PluginsHandler
+        plugin_handler = PluginsHandler()
+        from unmanic.libs.unplugins import PluginExecutor
+        plugin_ex = PluginExecutor()
+        for plugin_type in plugin_ex.get_all_plugin_types():
+            # Ignore types without flows
+            if not plugin_type.get('has_flow'):
+                continue
+
+            # Create list of plugins in this plugin type
+            plugin_flow[plugin_type.get('id')] = []
+            plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type(plugin_type.get('id'), library_id=self.model.id)
+            for plugin_module in plugin_modules:
+                plugin_flow[plugin_type.get('id')].append(
+                    {
+                        "plugin_id":   plugin_module.get("plugin_id"),
+                        "name":        plugin_module.get("name", ""),
+                        "author":      plugin_module.get("author", ""),
+                        "description": plugin_module.get("description", ""),
+                        "version":     plugin_module.get("version", ""),
+                        "icon":        plugin_module.get("icon", ""),
+                    }
+                )
+
+        return plugin_flow
+
+    def __set_default_plugin_flow_priority(self, plugin_list):
+        from unmanic.libs.unplugins import PluginExecutor
+        plugin_executor = PluginExecutor()
+        from unmanic.libs.plugins import PluginsHandler
+        plugin_handler = PluginsHandler()
+
+        # Fetch current items
+        configured_plugin_ids = []
+        query = LibraryPluginFlow.select().where(LibraryPluginFlow.library_id == self.model.id)
+        for flow_item in query:
+            configured_plugin_ids.append(flow_item.plugin_id.plugin_id)
+
+        for plugin in plugin_list:
+            # Ignore already configured plugins
+            if plugin.get('plugin_id') in configured_plugin_ids:
+                continue
+            plugin_info = plugin_handler.get_plugin_info(plugin.get('plugin_id'))
+            plugin_priorities = plugin_info.get('priorities')
+            if plugin_priorities:
+                # Fetch the plugin info back from the DB
+                plugin_info = Plugins.select().where(Plugins.plugin_id == plugin.get("plugin_id")).first()
+                # Fetch all plugin types in this plugin
+                plugin_types_in_plugin = plugin_executor.get_all_plugin_types_in_plugin(plugin.get("plugin_id"))
+                # Loop over the plugin types in this plugin
+                for plugin_type in plugin_types_in_plugin:
+                    # get the plugin runner function name for this runner
+                    plugin_type_meta = plugin_executor.get_plugin_type_meta(plugin_type)
+                    runner_string = plugin_type_meta.plugin_runner()
+                    if plugin_priorities.get(runner_string) and int(plugin_priorities.get(runner_string, 0)) > 0:
+                        # If the runner has a priority set and that value is greater than 0 (default that wont set anything),
+                        # Save the priority
+                        PluginsHandler.set_plugin_flow_position_for_single_plugin(
+                            plugin_info,
+                            plugin_type,
+                            self.model.id,
+                            plugin_priorities.get(runner_string)
+                        )
+
+    def set_enabled_plugins(self, plugin_list: list):
+        """
+        Update the list of enabled plugins
+
+        :param plugin_list:
+        :return:
+        """
+        # Remove all enabled plugins
+        self.__remove_enabled_plugins()
+
+        # Add new repos
+        data = []
+        plugin_ids = []
+        for plugin_info in plugin_list:
+            plugin = Plugins.get(plugin_id=plugin_info.get('plugin_id'))
+            plugin_ids.append(plugin.id)
+            if plugin:
+                data.append({
+                    "library_id":  self.model.id,
+                    "plugin_id":   plugin,
+                    "plugin_name": plugin.name,
+                })
+
+        # Delete all plugin flows for plugins not to be enabled for this library
+        self.__trim_plugin_flow(plugin_ids)
+
+        # Insert plugins
+        EnabledPlugins.insert_many(data).execute()
+
+        # Add default flow for newly added plugins
+        self.__set_default_plugin_flow_priority(plugin_list)
+
+    def save(self):
+        """
+        Save the data for this library
+
+        :return:
+        """
+        # Save changes made to model
+        self.model.save()
+
+        # If this is the default library path, save to config.library_path object also
+        if self.get_id() == 1:
+            config = Config()
+            config.set_config_item('library_path', self.get_path())
+
+    def delete(self):
+        """
+        Delete the current library
+
+        :return:
+        """
+        # Ensure we can never delete library ID 1 (the default library)
+        if self.get_id() == 1:
+            raise Exception("Unable remove the default library")
+
+        # Remove all enabled plugins
+        self.__remove_enabled_plugins()
+
+        # Remove all plugin flows
+        self.__trim_plugin_flow([])
+
+        # Delete all tasks with matching library_id
+        self.__remove_associated_tasks()
+
+        # Remove the library entry
+        return self.model.delete_instance(recursive=True)
diff --git a/unmanic/libs/libraryscanner.py b/unmanic/libs/libraryscanner.py
index e3334268..1106c842 100644
--- a/unmanic/libs/libraryscanner.py
+++ b/unmanic/libs/libraryscanner.py
@@ -42,6 +42,8 @@
 from unmanic import config
 from unmanic.libs import common, unlogger
 from unmanic.libs.filetest import FileTesterThread
+from unmanic.libs.library import Library
+from unmanic.libs.plugins import PluginsHandler
 
 
 class LibraryScannerManager(threading.Thread):
@@ -143,25 +145,49 @@ def run(self):
         self._log("Leaving LibraryScanner Monitor loop...")
 
     def scheduled_job(self):
-        from unmanic.libs.plugins import PluginsHandler
-        plugin_handler = PluginsHandler()
-        incompatible_plugins = plugin_handler.get_incompatible_enabled_plugins(self.data_queues.get('frontend_messages'))
-        if incompatible_plugins:
-            self._log("Skipping library scanner due incompatible plugins.", level='warning')
-            for incompatible_plugin in incompatible_plugins:
-                self._log("Found incompatible plugin '{}'".format(incompatible_plugin.get('plugin_id')), level='warning')
-            return
-        if not plugin_handler.within_enabled_plugin_limits(self.data_queues.get('frontend_messages')):
-            return
-        self._log("Running full library scan")
-        self.scan_library_path(self.settings.get_library_path())
+        """
+        Function called by the scheduled task
 
-    def add_path_to_queue(self, pathname):
-        self.scheduledtasks.put(pathname)
+        :return:
+        """
+        if not self.system_configuration_is_valid():
+            self._log("Skipping library scanner due invalid system configuration.", level='warning')
+            return
 
-    def start_results_manager_thread(self, manager_id, status_updates):
+        # For each configured library, check if a library scan is required
+        for lib_info in Library.get_all_libraries():
+            library = Library(lib_info['id'])
+            # Check if library scanner is enabled on any library
+            if library.get_enable_scanner():
+                # Run library scan
+                self._log("Running full library scan on library '{}'".format(library.get_name()))
+                self.scan_library_path(library.get_path(), library.get_id())
+        else:
+            self._log("No libraries are configured to run a library scan")
+
+    def system_configuration_is_valid(self):
+        """
+        Check and ensure the system configuration is correct for running
+
+        :return:
+        """
+        valid = True
+        plugin_handler = PluginsHandler()
+        if plugin_handler.get_incompatible_enabled_plugins(self.data_queues.get('frontend_messages')):
+            valid = False
+        if not Library.within_library_count_limits(self.data_queues.get('frontend_messages')):
+            valid = False
+        return valid
+
+    def add_path_to_queue(self, pathname, library_id):
+        self.scheduledtasks.put({
+            'pathname':   pathname,
+            'library_id': library_id,
+        })
+
+    def start_results_manager_thread(self, manager_id, status_updates, library_id):
         manager = FileTesterThread("FileTesterThread-{}".format(manager_id), self.files_to_test,
-                                   self.files_to_process, status_updates)
+                                   self.files_to_process, status_updates, library_id)
         manager.daemon = True
         manager.start()
         self.file_test_managers[manager_id] = manager
@@ -170,12 +196,19 @@ def stop_all_file_test_managers(self):
         for manager_id in self.file_test_managers:
             self.file_test_managers[manager_id].abort_flag.set()
 
-    def scan_library_path(self, search_folder):
-        if not os.path.exists(search_folder):
-            self._log("Path does not exist - '{}'".format(search_folder), level="warning")
+    def scan_library_path(self, library_path, library_id):
+        """
+        Run a scan of the given library path
+
+        :param library_path:
+        :param library_id:
+        :return:
+        """
+        if not os.path.exists(library_path):
+            self._log("Path does not exist - '{}'".format(library_path), level="warning")
             return
         if self.settings.get_debugging():
-            self._log("Scanning directory - '{}'".format(search_folder), level="debug")
+            self._log("Scanning directory - '{}'".format(library_path), level="debug")
 
         # Push status notification to frontend
         frontend_messages = self.data_queues.get('frontend_messages')
@@ -185,7 +218,7 @@ def scan_library_path(self, search_folder):
         status_updates = queue.Queue()
         self.file_test_managers = {}
         for results_manager_id in range(int(concurrent_file_testers)):
-            self.start_results_manager_thread(results_manager_id, status_updates)
+            self.start_results_manager_thread(results_manager_id, status_updates, library_id)
 
         start_time = time.time()
 
@@ -194,7 +227,7 @@ def scan_library_path(self, search_folder):
                 'id':      'libraryScanProgress',
                 'type':    'status',
                 'code':    'libraryScanProgress',
-                'message': "Scanning directory - '{}'".format(search_folder),
+                'message': "Scanning directory - '{}'".format(library_path),
                 'timeout': 0
             }
         )
@@ -203,7 +236,7 @@ def scan_library_path(self, search_folder):
         total_file_count = 0
         current_file = ''
         percent_completed_string = ''
-        for root, subFolders, files in os.walk(search_folder, followlinks=follow_symlinks):
+        for root, subFolders, files in os.walk(library_path, followlinks=follow_symlinks):
             if self.abort_flag.is_set():
                 break
             if self.settings.get_debugging():
@@ -269,7 +302,7 @@ def scan_library_path(self, search_folder):
                 current_file = status_updates.get()
                 continue
             elif not self.files_to_process.empty():
-                self.add_path_to_queue(self.files_to_process.get())
+                self.add_path_to_queue(self.files_to_process.get(), library_id)
                 continue
             else:
                 time.sleep(.1)
diff --git a/unmanic/libs/plugins.py b/unmanic/libs/plugins.py
index 613244b8..b5872417 100644
--- a/unmanic/libs/plugins.py
+++ b/unmanic/libs/plugins.py
@@ -40,10 +40,10 @@
 
 from unmanic import config
 from unmanic.libs import common, unlogger
+from unmanic.libs.library import Library
 from unmanic.libs.session import Session
 from unmanic.libs.singleton import SingletonType
-from unmanic.libs.unmodels import Plugins, PluginRepos
-from unmanic.libs.unmodels.pluginflow import PluginFlow
+from unmanic.libs.unmodels import EnabledPlugins, LibraryPluginFlow, Plugins, PluginRepos
 from unmanic.libs.unplugins import PluginExecutor
 
 
@@ -52,7 +52,7 @@ class PluginsHandler(object, metaclass=SingletonType):
     Set plugin version.
     Plugins must be compatible with this version to be installed.
     """
-    version: int = 1
+    version: int = 2
 
     """
     Set the default repo to main repo
@@ -169,7 +169,7 @@ def update_plugin_repos(self):
 
         return True
 
-    def get_settings_of_all_enabled_plugins(self):
+    def get_settings_of_all_installed_plugins(self):
         all_settings = {}
 
         # First fetch all enabled plugins
@@ -179,11 +179,11 @@ def get_settings_of_all_enabled_plugins(self):
                 "dir":    'asc',
             },
         ]
-        enabled_plugins = self.get_plugin_list_filtered_and_sorted(order=order, enabled=True)
+        installed_plugins = self.get_plugin_list_filtered_and_sorted(order=order)
 
         # Fetch settings for each plugin
         plugin_executor = PluginExecutor()
-        for plugin in enabled_plugins:
+        for plugin in installed_plugins:
             plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(plugin.get('plugin_id'))
             all_settings[plugin.get('plugin_id')] = plugin_settings
 
@@ -337,7 +337,7 @@ def install_plugin_by_id(self, plugin_id, repo_id=None):
         :param repo_id:
         :return:
         """
-        plugin_list = self.get_installable_plugins_list(repo_id)
+        plugin_list = self.get_installable_plugins_list(filter_repo_id=repo_id)
         for plugin in plugin_list:
             if plugin.get('plugin_id') == plugin_id:
                 success = self.install_plugin(plugin)
@@ -421,32 +421,8 @@ def write_plugin_data_to_db(plugin, plugin_directory):
             update_query.execute()
         else:
             # Insert a new entry
-            # Plugins are disable when first installed. This will help to prevent issues with broken plugins
-            plugin_data[Plugins.enabled] = False
             Plugins.insert(plugin_data).execute()
 
-            # On first install, set the priority for each runner
-            plugin_priorities = plugin.get('priorities')
-            if plugin_priorities:
-                # Fetch the plugin info back from the DB
-                plugin_info = Plugins.select().where(Plugins.plugin_id == plugin.get("plugin_id")).first()
-                # Fetch all plugin types in this plugin
-                plugin_executor = PluginExecutor()
-                plugin_types_in_plugin = plugin_executor.get_all_plugin_types_in_plugin(plugin.get("plugin_id"))
-                # Loop over the plugin types in this plugin
-                for plugin_type in plugin_types_in_plugin:
-                    # get the plugin runner function name for this runner
-                    plugin_type_meta = plugin_executor.get_plugin_type_meta(plugin_type)
-                    runner_string = plugin_type_meta.plugin_runner()
-                    if plugin_priorities.get(runner_string) and int(plugin_priorities.get(runner_string, 0)) > 0:
-                        # If the runner has a priority set and that value is greater than 0 (default that wont set anything),
-                        # Save the priority
-                        PluginsHandler.set_plugin_flow_position_for_single_plugin(
-                            plugin_info,
-                            plugin_type,
-                            plugin_priorities.get(runner_string)
-                        )
-
         return True
 
     def get_total_plugin_list_count(self):
@@ -454,13 +430,19 @@ def get_total_plugin_list_count(self):
         return task_query.count()
 
     def get_plugin_list_filtered_and_sorted(self, order=None, start=0, length=None, search_value=None, id_list=None,
-                                            enabled=None, plugin_id=None, plugin_type=None):
+                                            enabled=None, plugin_id=None, plugin_type=None, library_id=None):
         try:
             query = (Plugins.select())
 
             if plugin_type:
-                join_condition = ((PluginFlow.plugin_id == Plugins.id) & (PluginFlow.plugin_type == plugin_type))
-                query = query.join(PluginFlow, join_type='LEFT OUTER JOIN', on=join_condition)
+                if library_id is not None:
+                    join_condition = (
+                        (LibraryPluginFlow.plugin_id == Plugins.id) & (LibraryPluginFlow.plugin_type == plugin_type) & (
+                        LibraryPluginFlow.library_id == library_id))
+                else:
+                    join_condition = (
+                        (LibraryPluginFlow.plugin_id == Plugins.id) & (LibraryPluginFlow.plugin_type == plugin_type))
+                query = query.join(LibraryPluginFlow, join_type='LEFT OUTER JOIN', on=join_condition)
 
             if id_list:
                 query = query.where(Plugins.id.in_(id_list))
@@ -472,8 +454,15 @@ def get_plugin_list_filtered_and_sorted(self, order=None, start=0, length=None,
             if plugin_id is not None:
                 query = query.where(Plugins.plugin_id.in_([plugin_id]))
 
+            # Deprecate this "enabled" status as plugins are now enabled when the are assigned to a library
             if enabled is not None:
-                query = query.where(Plugins.enabled == enabled)
+                raise Exception("Fetching plugins by 'enabled' status is deprecated")
+
+            if library_id is not None:
+                join_condition = (
+                    (EnabledPlugins.plugin_id == Plugins.id) & (EnabledPlugins.library_id == library_id))
+                query = query.join(EnabledPlugins, join_type='LEFT OUTER JOIN', on=join_condition)
+                query = query.where(EnabledPlugins.plugin_id != None)
 
             # Get order by
             if order:
@@ -498,52 +487,6 @@ def get_plugin_list_filtered_and_sorted(self, order=None, start=0, length=None,
             # No plugin entries exist yet
             self._log("No plugins exist yet.", level="warning")
 
-    def enable_plugin_by_db_table_id(self, plugin_table_ids, frontend_messages=None):
-        self._log("Enable plugins '{}'".format(plugin_table_ids), level='debug')
-
-        # Refresh session
-        s = Session()
-        s.register_unmanic()
-
-        # Enable the matching entries in the table
-        Plugins.update(enabled=True).where(Plugins.id.in_(plugin_table_ids)).execute()
-
-        # Fetch records
-        records_by_id = self.get_plugin_list_filtered_and_sorted(id_list=plugin_table_ids)
-
-        # Ensure they are now enabled
-        plugin_executor = PluginExecutor()
-        for record in records_by_id:
-            if record.get('enabled'):
-                # Reload the plugin module
-                plugin_executor.reload_plugin_module(record.get('plugin_id'))
-                continue
-            self._log("Failed to enable plugin '{}'".format(record.get('plugin_id')), level='debug')
-            return False
-
-        # Warn on any incompatible enabled plugins
-        self.get_incompatible_enabled_plugins(frontend_messages)
-        self.within_enabled_plugin_limits(frontend_messages)
-
-        return True
-
-    def disable_plugin_by_db_table_id(self, plugin_table_ids):
-        self._log("Disable plugins '{}'".format(plugin_table_ids), level='debug')
-        # Disable the matching entries in the table
-        Plugins.update(enabled=False).where(Plugins.id.in_(plugin_table_ids)).execute()
-
-        # Fetch records
-        records_by_id = self.get_plugin_list_filtered_and_sorted(id_list=plugin_table_ids)
-
-        # Ensure they are now disabled
-        for record in records_by_id:
-            if not record.get('enabled'):
-                continue
-            self._log("Failed to disable plugin '{}'".format(record.get('plugin_id')), level='debug')
-            return False
-
-        return True
-
     def flag_plugin_for_update_by_id(self, plugin_id):
         self._log("Flagging update available for installed plugin '{}'".format(plugin_id), level='debug')
         # Disable the matching entries in the table
@@ -576,6 +519,14 @@ def uninstall_plugins_by_db_table_id(self, plugin_table_ids: list):
 
         # Remove each plugin from disk
         for record in records_by_id:
+            # Unload plugin modules
+            try:
+                PluginExecutor.unload_plugin_module(record.get('plugin_id'))
+            except Exception as e:
+                self._log("Exception while unloading python module {}:".format(record.get('plugin_id')), message2=str(e),
+                          level="exception")
+
+            # Remove from disk
             plugin_directory = self.get_plugin_path(record.get('plugin_id'))
             self._log("Removing plugin files from disk '{}'".format(plugin_directory), level='debug')
             try:
@@ -590,8 +541,8 @@ def uninstall_plugins_by_db_table_id(self, plugin_table_ids: list):
                 self._log("Exception while removing directory {}:".format(plugin_directory), message2=str(e),
                           level="exception")
 
-            # Unload plugin modules
-            PluginExecutor.unload_plugin_module(record.get('plugin_id'))
+        # Unlink from library by ID in DB
+        EnabledPlugins.delete().where(EnabledPlugins.plugin_id.in_(plugin_table_ids)).execute()
 
         # Delete by ID in DB
         if not Plugins.delete().where(Plugins.id.in_(plugin_table_ids)).execute():
@@ -614,16 +565,18 @@ def update_plugins_by_db_table_id(self, plugin_table_ids):
 
         return True
 
-    def set_plugin_flow(self, plugin_type, flow):
+    def set_plugin_flow(self, plugin_type, library_id, flow):
         """
         Update the plugin flow for all plugins in a given plugin type
 
         :param plugin_type:
+        :param library_id:
         :param flow:
         :return:
         """
         # Delete all current flow data for this plugin type
-        delete_query = PluginFlow.delete().where(PluginFlow.plugin_type == plugin_type)
+        delete_query = LibraryPluginFlow.delete().where(
+            (LibraryPluginFlow.plugin_type == plugin_type) & (LibraryPluginFlow.library_id == library_id))
         delete_query.execute()
 
         success = True
@@ -637,7 +590,7 @@ def set_plugin_flow(self, plugin_type, flow):
                 continue
 
             # Save the plugin flow
-            plugin_flow = self.set_plugin_flow_position_for_single_plugin(plugin_info, plugin_type, priority)
+            plugin_flow = self.set_plugin_flow_position_for_single_plugin(plugin_info, plugin_type, library_id, priority)
             priority += 1
 
             if not plugin_flow:
@@ -646,12 +599,13 @@ def set_plugin_flow(self, plugin_type, flow):
         return success
 
     @staticmethod
-    def set_plugin_flow_position_for_single_plugin(plugin_info: Plugins, plugin_type: str, priority: int):
+    def set_plugin_flow_position_for_single_plugin(plugin_info: Plugins, plugin_type: str, library_id: int, priority: int):
         """
         Update the plugin flow for a single plugin and type with the provided priority.
 
         :param plugin_info:
         :param plugin_type:
+        :param library_id:
         :param priority:
         :return:
         """
@@ -659,22 +613,27 @@ def set_plugin_flow_position_for_single_plugin(plugin_info: Plugins, plugin_type
         # Save the plugin flow
         flow_dict = {
             'plugin_id':   plugin_info.id,
+            'library_id':  library_id,
             'plugin_name': plugin_info.plugin_id,
             'plugin_type': plugin_type,
             'position':    priority,
         }
-        plugin_flow = PluginFlow.create(**flow_dict)
+        plugin_flow = LibraryPluginFlow.create(**flow_dict)
 
         return plugin_flow
 
-    def get_enabled_plugin_modules_by_type(self, plugin_type):
+    def get_enabled_plugin_modules_by_type(self, plugin_type, library_id=None):
         """
         Return a list of enabled plugin modules when given a plugin type
 
         Runners are filtered by the given 'plugin_type' and sorted by
         configured order of execution.
 
+        If no library ID is provided, this will return all installed plugins for that type.
+        This case should only be used for plugin runner types that are not associated with a library.
+
         :param plugin_type:
+        :param library_id:
         :return:
         """
         # Refresh session
@@ -684,7 +643,7 @@ def get_enabled_plugin_modules_by_type(self, plugin_type):
         # First fetch all enabled plugins
         order = [
             {
-                "model":  PluginFlow,
+                "model":  LibraryPluginFlow,
                 "column": 'position',
                 "dir":    'asc',
             },
@@ -693,10 +652,7 @@ def get_enabled_plugin_modules_by_type(self, plugin_type):
                 "dir":    'asc',
             },
         ]
-        enabled_plugins = self.get_plugin_list_filtered_and_sorted(order=order, enabled=True, plugin_type=plugin_type)
-        # Validate enabled plugins
-        if not (s.level > 1) and (len(enabled_plugins) > s.plugin_count):
-            enabled_plugins = []
+        enabled_plugins = self.get_plugin_list_filtered_and_sorted(order=order, plugin_type=plugin_type, library_id=library_id)
 
         # Fetch all plugin modules from the given list of enabled plugins
         plugin_executor = PluginExecutor()
@@ -717,42 +673,6 @@ def exec_plugin_runner(self, data, plugin_id, plugin_type):
         plugin_executor = PluginExecutor()
         return plugin_executor.execute_plugin_runner(data, plugin_id, plugin_type)
 
-    def within_enabled_plugin_limits(self, frontend_messages=None):
-        """
-        Ensure enabled plugins are within limits
-
-        :param frontend_messages:
-        :return:
-        """
-        # Fetch level from session
-        s = Session()
-        s.register_unmanic()
-        if s.level > 1:
-            return True
-
-        # Fetch all enabled plugins
-        enabled_plugins = self.get_plugin_list_filtered_and_sorted(enabled=True)
-
-        def add_frontend_message():
-            # If the frontend messages queue was included in request, append a message
-            if frontend_messages:
-                frontend_messages.put(
-                    {
-                        'id':      'pluginEnabledLimits',
-                        'type':    'error',
-                        'code':    'pluginEnabledLimits',
-                        'message': '',
-                        'timeout': 0
-                    }
-                )
-
-        # Ensure enabled plugins are within limits
-        # Function was returned above if the user was logged in and able to use infinite
-        if len(enabled_plugins) > s.plugin_count:
-            add_frontend_message()
-            return False
-        return True
-
     def get_incompatible_enabled_plugins(self, frontend_messages=None):
         """
         Ensure that the currently installed plugins are compatible with this PluginsHandler version
@@ -761,8 +681,8 @@ def get_incompatible_enabled_plugins(self, frontend_messages=None):
         :return:
         :rtype:
         """
-        # Fetch all enabled plugins
-        enabled_plugins = self.get_plugin_list_filtered_and_sorted(enabled=True)
+        # Fetch all libraries
+        all_libraries = Library.get_all_libraries()
 
         def add_frontend_message(plugin_id, name):
             # If the frontend messages queue was included in request, append a message
@@ -777,30 +697,34 @@ def add_frontend_message(plugin_id, name):
                     }
                 )
 
-        # Ensure only compatible plugins are enabled
-        # If all enabled plugins are compatible, then return true
+        # Fetch all enabled plugins
         incompatible_list = []
-        for record in enabled_plugins:
-            try:
-                # Ensure plugin is compatible
-                plugin_info = self.get_plugin_info(record.get('plugin_id'))
-            except Exception as e:
-                plugin_info = None
-                self._log("Exception while fetching plugin info for {}:".format(record.get('plugin_id')), message2=str(e),
-                          level="exception")
-            if plugin_info:
-                # Plugins will require a 'compatibility' entry in their info.json file.
-                #   This must list the plugin handler versions that it is compatible with
-                if self.version in plugin_info.get('compatibility', []):
-                    continue
+        for library in all_libraries:
+            enabled_plugins = self.get_plugin_list_filtered_and_sorted(library_id=library.get('id'))
+
+            # Ensure only compatible plugins are enabled
+            # If all enabled plugins are compatible, then return true
+            for record in enabled_plugins:
+                try:
+                    # Ensure plugin is compatible
+                    plugin_info = self.get_plugin_info(record.get('plugin_id'))
+                except Exception as e:
+                    plugin_info = None
+                    self._log("Exception while fetching plugin info for {}:".format(record.get('plugin_id')), message2=str(e),
+                              level="exception")
+                if plugin_info:
+                    # Plugins will require a 'compatibility' entry in their info.json file.
+                    #   This must list the plugin handler versions that it is compatible with
+                    if self.version in plugin_info.get('compatibility', []):
+                        continue
 
-            incompatible_list.append(
-                {
-                    'plugin_id': record.get('plugin_id'),
-                    'name':      record.get('name'),
-                }
-            )
-            add_frontend_message(record.get('plugin_id'), record.get('name'))
+                incompatible_list.append(
+                    {
+                        'plugin_id': record.get('plugin_id'),
+                        'name':      record.get('name'),
+                    }
+                )
+                add_frontend_message(record.get('plugin_id'), record.get('name'))
 
         return incompatible_list
 
diff --git a/unmanic/libs/postprocessor.py b/unmanic/libs/postprocessor.py
index e94bf351..632af9a0 100644
--- a/unmanic/libs/postprocessor.py
+++ b/unmanic/libs/postprocessor.py
@@ -36,6 +36,7 @@
 
 from unmanic import config
 from unmanic.libs import common, history
+from unmanic.libs.library import Library
 from unmanic.libs.plugins import PluginsHandler
 
 """
@@ -86,7 +87,7 @@ def run(self):
         while not self.abort_flag.is_set():
             time.sleep(1)
 
-            if not self.all_plugins_are_compatible():
+            if not self.system_configuration_is_valid():
                 time.sleep(2)
                 continue
 
@@ -115,13 +116,17 @@ def run(self):
 
         self._log("Leaving PostProcessor Monitor loop...")
 
-    def all_plugins_are_compatible(self):
-        """Ensure all plugins are compatible before running"""
+    def system_configuration_is_valid(self):
+        """
+        Check and ensure the system configuration is correct for running
+
+        :return:
+        """
         valid = True
         plugin_handler = PluginsHandler()
         if plugin_handler.get_incompatible_enabled_plugins(self.data_queues.get('frontend_messages')):
             valid = False
-        if not plugin_handler.within_enabled_plugin_limits(self.data_queues.get('frontend_messages')):
+        if not Library.within_library_count_limits(self.data_queues.get('frontend_messages')):
             valid = False
         return valid
 
@@ -131,6 +136,7 @@ def post_process_file(self):
 
         # Read current task data
         # task_data = self.current_task.get_task_data()
+        library_id = self.current_task.get_task_library()
         cache_path = self.current_task.get_cache_path()
         source_data = self.current_task.get_source_data()
         destination_data = self.current_task.get_destination_data()
@@ -142,7 +148,8 @@ def post_process_file(self):
             # Run a postprocess file movement on the cache file for for each plugin that configures it
 
             # Fetch all 'postprocessor.file_move' plugin modules
-            plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type('postprocessor.file_move')
+            plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type('postprocessor.file_move',
+                                                                               library_id=library_id)
 
             # Check if the source file needs to be remove by default (only if it does not match the destination file)
             remove_source_file = False
@@ -151,11 +158,12 @@ def post_process_file(self):
 
             # Set initial data (some fields will be overwritten further down)
             data = {
-                "source_data":        None,
+                'library_id':         library_id,
+                'source_data':        None,
                 'remove_source_file': remove_source_file,
                 'copy_file':          None,
-                "file_in":            None,
-                "file_out":           None,
+                'file_in':            None,
+                'file_out':           None,
             }
 
             for plugin_module in plugin_modules:
@@ -216,11 +224,12 @@ def post_process_file(self):
                       level='warning')
 
         # Fetch all 'postprocessor.task_result' plugin modules
-        plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type('postprocessor.task_result')
+        plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type('postprocessor.task_result', library_id=library_id)
 
         for plugin_module in plugin_modules:
             data = {
-                "source_data":                 source_data,
+                'library_id':                  library_id,
+                'source_data':                 source_data,
                 'task_processing_success':     self.current_task.task.success,
                 'file_move_processes_success': file_move_processes_success,
                 'destination_files':           destination_files,
diff --git a/unmanic/libs/session.py b/unmanic/libs/session.py
index 7f096217..33197189 100644
--- a/unmanic/libs/session.py
+++ b/unmanic/libs/session.py
@@ -54,14 +54,14 @@ class Session(object, metaclass=SingletonType):
     level = 0
 
     """
-    non supporter plugin count
+    non supporter library count
     """
-    plugin_count = 20
+    library_count = 2
 
     """
     non supporter linked installations count
     """
-    link_count = 1
+    link_count = 5
 
     """
     picture_uri - The user avatar
@@ -282,6 +282,14 @@ def get_patreon_login_url(self):
         """
         return "{0}/patreon-login".format(self.get_site_url())
 
+    def get_github_login_url(self):
+        """
+        Fetch the Patreon client ID
+
+        :return:
+        """
+        return "{0}/github-login".format(self.get_site_url())
+
     def get_patreon_sponsor_page(self):
         """
         Fetch the Patreon sponsor page
diff --git a/unmanic/libs/task.py b/unmanic/libs/task.py
index 039f4f35..dd8bda62 100644
--- a/unmanic/libs/task.py
+++ b/unmanic/libs/task.py
@@ -109,7 +109,7 @@ def get_task_data(self):
 
     def get_task_id(self):
         if not self.task:
-            raise Exception('Unable to fetch task type. Task has not been set!')
+            raise Exception('Unable to fetch task ID. Task has not been set!')
         return self.task.id
 
     def get_task_type(self):
@@ -117,6 +117,11 @@ def get_task_type(self):
             raise Exception('Unable to fetch task type. Task has not been set!')
         return self.task.type
 
+    def get_task_library(self):
+        if not self.task:
+            raise Exception('Unable to fetch task library ID. Task has not been set!')
+        return self.task.library_id
+
     def get_cache_path(self):
         if not self.task:
             raise Exception('Unable to fetch cache path. Task has not been set!')
@@ -178,7 +183,7 @@ def read_and_set_task_by_absolute_path(self, abspath):
         # Get task matching the abspath
         self.task = Tasks.get(abspath=abspath)
 
-    def create_task_by_absolute_path(self, abspath, task_type='local'):
+    def create_task_by_absolute_path(self, abspath, task_type='local', library_id=1):
         """
         Creates the task by it's absolute path.
         If the task already exists in the list, then this will throw an exception and return false
@@ -187,10 +192,11 @@ def create_task_by_absolute_path(self, abspath, task_type='local'):
 
         :param abspath:
         :param task_type:
+        :param library_id:
         :return:
         """
         try:
-            self.task = Tasks.create(abspath=abspath, status='creating')
+            self.task = Tasks.create(abspath=abspath, status='creating', library_id=library_id)
             self.save()
             self._log("Created new task with ID: {} for {}".format(self.task, abspath), level="debug")
 
diff --git a/unmanic/libs/taskhandler.py b/unmanic/libs/taskhandler.py
index 8b70bbd1..efca183f 100644
--- a/unmanic/libs/taskhandler.py
+++ b/unmanic/libs/taskhandler.py
@@ -86,8 +86,10 @@ def run(self):
     def process_scheduledtasks_queue(self):
         while not self.abort_flag.is_set() and not self.scheduledtasks.empty():
             try:
-                pathname = self.scheduledtasks.get_nowait()
-                if self.add_path_to_task_queue(pathname):
+                item = self.scheduledtasks.get_nowait()
+                pathname = item['pathname']
+                library_id = item['library_id']
+                if self.add_path_to_task_queue(pathname, library_id):
                     self._log("Adding file to task queue", pathname, level='info')
                 else:
                     self._log("Skipping file as it is already in the queue", pathname, level='info')
@@ -99,10 +101,12 @@ def process_scheduledtasks_queue(self):
     def process_inotifytasks_queue(self):
         while not self.abort_flag.is_set() and not self.inotifytasks.empty():
             try:
-                pathname = self.inotifytasks.get_nowait()
+                item = self.inotifytasks.get_nowait()
+                pathname = item['pathname']
+                library_id = item['library_id']
                 # TODO: Ensure the file is not still being modified at this point.
                 #  If it is still being modified here, it is ok to wait for that to finish (should not matter much)
-                if self.add_path_to_task_queue(pathname):
+                if self.add_path_to_task_queue(pathname, library_id):
                     self._log("Adding inotify job to queue", pathname, level='info')
                 else:
                     self._log("Skipping inotify job already in the queue", pathname, level='info')
@@ -119,30 +123,38 @@ def clear_tasks_on_startup(self):
         rows_deleted_count = query.execute()
         self._log("Deleted {} items from tasks list".format(rows_deleted_count), level='debug')
 
-    def add_path_to_task_queue(self, pathname):
+    def add_path_to_task_queue(self, pathname, library_id):
+        """
+        Add the path to the task queue ensuring that the path is only added once
+
+        :param pathname:
+        :param library_id:
+        :return:
+        """
         # Check if file exists in task queue based on it's absolute path
         abspath = os.path.abspath(pathname)
         existing_task_query = Tasks.select().where((Tasks.abspath == abspath)).limit(1)
         if existing_task_query.count() > 0:
             return False
         # Create the new task from the provide path
-        new_task = self.create_task_from_path(pathname)
+        new_task = self.create_task_from_path(pathname, library_id)
         if not new_task:
             return False
         return True
 
-    def create_task_from_path(self, pathname):
+    def create_task_from_path(self, pathname, library_id):
         """
         Generate a Task object from a pathname
 
         :param pathname:
+        :param library_id:
         :return:
         """
         abspath = os.path.abspath(pathname)
         # Create a new task
         new_task = task.Task()
 
-        if not new_task.create_task_by_absolute_path(abspath):
+        if not new_task.create_task_by_absolute_path(abspath, library_id=library_id):
             # If file exists in task queue already this will return false.
             # Do not carry on.
             return False
diff --git a/unmanic/libs/unmodels/__init__.py b/unmanic/libs/unmodels/__init__.py
index b52cb4dc..0080e2bc 100644
--- a/unmanic/libs/unmodels/__init__.py
+++ b/unmanic/libs/unmodels/__init__.py
@@ -34,11 +34,12 @@
 
 from .completedtaskscommandlogs import CompletedTasksCommandLogs
 from .completedtasks import CompletedTasks
+from .enabledplugins import EnabledPlugins
 from .installation import Installation
-from .pluginflow import PluginFlow
 from .pluginrepos import PluginRepos
 from .plugins import Plugins
-from .settings import Settings
+from .libraries import Libraries
+from .librarypluginflow import LibraryPluginFlow
 from .tasks import Tasks
 
 __author__ = 'Josh.5 (jsunnex@gmail.com)'
@@ -46,11 +47,12 @@
 __all__ = (
     'CompletedTasks',
     'CompletedTasksCommandLogs',
+    'EnabledPlugins',
     'Installation',
-    'PluginFlow',
+    'Libraries',
+    'LibraryPluginFlow',
     'PluginRepos',
     'Plugins',
-    'Settings',
     'Tasks',
 )
 
diff --git a/unmanic/libs/unmodels/enabledplugins.py b/unmanic/libs/unmodels/enabledplugins.py
new file mode 100644
index 00000000..c453c0f1
--- /dev/null
+++ b/unmanic/libs/unmodels/enabledplugins.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+    unmanic.enabledplugins.py
+
+    Written by:               Josh.5 <jsunnex@gmail.com>
+    Date:                     07 Feb 2022, (1:25 PM)
+
+    Copyright:
+           Copyright (C) Josh Sunnex - All Rights Reserved
+
+           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.
+
+"""
+
+from peewee import *
+
+from unmanic.libs.unmodels.lib import BaseModel
+from unmanic.libs.unmodels.libraries import Libraries
+from unmanic.libs.unmodels.plugins import Plugins
+
+
+class EnabledPlugins(BaseModel):
+    """
+    EnabledPlugins
+    """
+    library_id = ForeignKeyField(Libraries, backref='enabled_plugins', on_delete='CASCADE', on_update='CASCADE')
+    plugin_id = ForeignKeyField(Plugins, backref='enabled_libraries', on_delete='CASCADE', on_update='CASCADE')
+    plugin_name = TextField(null=False)
diff --git a/unmanic/libs/unmodels/libraries.py b/unmanic/libs/unmodels/libraries.py
new file mode 100644
index 00000000..56a03f07
--- /dev/null
+++ b/unmanic/libs/unmodels/libraries.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+    unmanic.libraries.py
+
+    Written by:               Josh.5 <jsunnex@gmail.com>
+    Date:                     06 Feb 2022, (9:44 AM)
+
+    Copyright:
+           Copyright (C) Josh Sunnex - All Rights Reserved
+
+           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.
+
+"""
+
+from peewee import *
+
+from unmanic.libs.unmodels.lib import BaseModel
+
+
+class Libraries(BaseModel):
+    """
+    Libraries
+    """
+    name = TextField(null=False, unique=True)
+    path = TextField(null=False)
+    enable_scanner = BooleanField(null=False, default=False)
+    enable_inotify = BooleanField(null=False, default=False)
diff --git a/unmanic/libs/unmodels/pluginflow.py b/unmanic/libs/unmodels/librarypluginflow.py
similarity index 85%
rename from unmanic/libs/unmodels/pluginflow.py
rename to unmanic/libs/unmodels/librarypluginflow.py
index b95aa01e..1ae9f29b 100644
--- a/unmanic/libs/unmodels/pluginflow.py
+++ b/unmanic/libs/unmodels/librarypluginflow.py
@@ -2,10 +2,10 @@
 # -*- coding: utf-8 -*-
 
 """
-    unmanic.pluginflow.py
+    unmanic.librarypluginflow.py
 
     Written by:               Josh.5 <jsunnex@gmail.com>
-    Date:                     19 Apr 2021, (8:47 PM)
+    Date:                     13 Feb 2022, (11:27 AM)
 
     Copyright:
            Copyright (C) Josh Sunnex - All Rights Reserved
@@ -33,14 +33,16 @@
 from peewee import *
 
 from unmanic.libs.unmodels.lib import BaseModel
+from unmanic.libs.unmodels.libraries import Libraries
 from unmanic.libs.unmodels.plugins import Plugins
 
 
-class PluginFlow(BaseModel):
+class LibraryPluginFlow(BaseModel):
     """
-    PluginFlow
+    LibraryPluginFlow
     """
     plugin_id = ForeignKeyField(Plugins, backref='flow', on_delete='CASCADE', on_update='CASCADE')
+    library_id = ForeignKeyField(Libraries, backref='plugin_flow', on_delete='CASCADE', on_update='CASCADE')
     plugin_name = TextField(null=False)
     plugin_type = TextField(null=False)
     position = IntegerField(null=False, default=9999)
diff --git a/unmanic/libs/unmodels/plugins.py b/unmanic/libs/unmodels/plugins.py
index 60cc063b..967577cb 100644
--- a/unmanic/libs/unmodels/plugins.py
+++ b/unmanic/libs/unmodels/plugins.py
@@ -46,6 +46,7 @@ class Plugins(BaseModel):
     description = TextField(null=False)
     icon = TextField(null=False)
     local_path = TextField(null=False)
-    enabled = BooleanField(null=False, default=False)
+    # NOTE: Enabled plugins here is deprecated. This is kept for compatibility with older DB
+    enabled = BooleanField(null=True, default=False)
     update_available = BooleanField(null=False, default=False)
     position = IntegerField(null=False, default=9999)
diff --git a/unmanic/libs/unmodels/settings.py b/unmanic/libs/unmodels/settings.py
deleted file mode 100644
index 413e51f4..00000000
--- a/unmanic/libs/unmodels/settings.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-    unmanic.settings.py
- 
-    Written by:               Josh.5 <jsunnex@gmail.com>
-    Date:                     22 Jun 2019, (1:43 PM)
- 
-    Copyright:
-           Copyright (C) Josh Sunnex - All Rights Reserved
- 
-           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.
-
-"""
-
-import os
-from peewee import *
-
-from unmanic.libs import common
-from unmanic.libs.unmodels.lib import BaseModel
-
-
-# TODO: Re-order model to match UI layout for easier debugging
-class Settings(BaseModel):
-    """
-    Settings
-
-    All application settings
-    """
-    audio_codec = TextField(null=False, default='aac')
-    audio_stream_encoder = TextField(null=False, default='aac')
-    audio_codec_cloning = TextField(null=False, default='aac')
-    audio_stream_encoder_cloning = TextField(null=False, default='aac')
-    audio_stereo_stream_bitrate = TextField(null=False, default='128k')
-    cache_path = TextField(null=False, default='/tmp/unmanic')
-    config_path = TextField(null=False, default=os.path.join(common.get_home_dir(), '.unmanic', 'config'))
-    keep_filename_history = BooleanField(null=False, default=True)
-    debugging = BooleanField(null=False, default=False)
-    enable_audio_encoding = BooleanField(null=False, default=True)
-    enable_audio_stream_transcoding = BooleanField(null=False, default=True)
-    enable_audio_stream_stereo_cloning = BooleanField(null=False, default=True)
-    enable_inotify = BooleanField(null=False, default=False)
-    enable_video_encoding = BooleanField(null=False, default=True)
-    library_path = TextField(null=False, default='/library')
-    log_path = TextField(null=False, default=os.path.join(common.get_home_dir(), '.unmanic', 'logs'))
-    number_of_workers = IntegerField(null=False, default=3)
-    keep_original_container = BooleanField(null=False, default=False)
-    out_container = TextField(null=False, default='matroska')
-    remove_subtitle_streams = BooleanField(null=False, default=False)
-    run_full_scan_on_start = BooleanField(null=False, default=False)
-    schedule_full_scan_minutes = IntegerField(null=False, default=1440)
-    search_extensions = TextField(null=False, default='mkv,avi,mov,ts,rmvb,mp4,')
-    video_codec = TextField(null=False, default='hevc')
-    video_stream_encoder = TextField(null=False, default='libx265')
-    overwrite_additional_ffmpeg_options = BooleanField(null=False, default=False)
-    additional_ffmpeg_options = TextField(null=True, default='')
-    enable_hardware_accelerated_decoding = BooleanField(null=False, default=False)
diff --git a/unmanic/libs/unmodels/tasks.py b/unmanic/libs/unmodels/tasks.py
index 664b4617..c6781d45 100644
--- a/unmanic/libs/unmodels/tasks.py
+++ b/unmanic/libs/unmodels/tasks.py
@@ -43,7 +43,8 @@ class Tasks(BaseModel):
     abspath = TextField(null=False, unique=True)
     cache_path = TextField(null=True, unique=True)
     priority = BigIntegerField(null=True)
-    type = TextField(null=False, default='local') # (local, remote)
+    type = TextField(null=False, default='local')  # (local, remote)
+    library_id = IntegerField(null=False, default=1)
     status = TextField(null=False)  # (pending, in_progress, processed)
     success = BooleanField(null=True)
     start_time = DateTimeField(null=True, default=datetime.datetime.now)
diff --git a/unmanic/libs/unplugins/executor.py b/unmanic/libs/unplugins/executor.py
index e2a856c6..ec3fca7c 100644
--- a/unmanic/libs/unplugins/executor.py
+++ b/unmanic/libs/unplugins/executor.py
@@ -38,7 +38,7 @@
 
 from . import plugin_types
 from unmanic.libs import unlogger, common
-from ..unmodels.pluginflow import PluginFlow
+from ..unmodels import LibraryPluginFlow
 
 
 class PluginExecutor(object):
@@ -88,27 +88,6 @@ def __get_plugin_directory(self, plugin_id):
         """
         return os.path.join(self.plugins_directory, plugin_id)
 
-    def __get_enabled_plugins(self, plugin_type=None):
-        """
-        Returns a list of enabled plugins
-
-        :return:
-        """
-        from unmanic.libs.plugins import PluginsHandler
-        plugin_handler = PluginsHandler()
-        order = [
-            {
-                "model":  PluginFlow,
-                "column": 'position',
-                "dir":    'asc',
-            },
-            {
-                "column": 'name',
-                "dir":    'asc',
-            },
-        ]
-        return plugin_handler.get_plugin_list_filtered_and_sorted(order=order, enabled=True, plugin_type=plugin_type)
-
     @staticmethod
     def __include_plugin_site_packages(path):
         plugin_site_packages_dir = os.path.join(path, 'site-packages')
@@ -174,7 +153,7 @@ def reload_plugin_module(self, plugin_id):
         """
         # Set the module name
         module_name = '{}.plugin'.format(plugin_id)
-        #self._log("Reloading module '{}'".format(module_name), level="debug")
+        # self._log("Reloading module '{}'".format(module_name), level="debug")
 
         if module_name in sys.modules:
             # Get all submodules
@@ -276,7 +255,7 @@ def execute_plugin_runner(self, data, plugin_id, plugin_type):
                           level="exception")
 
             del runner
-            #gc.collect()
+            # gc.collect()
 
         return run_successfully
 
@@ -355,11 +334,12 @@ def get_plugin_data_by_type(self, enabled_plugins, plugin_type):
         # Return runners
         return plugin_data
 
-    def get_plugin_settings(self, plugin_id):
+    def get_plugin_settings(self, plugin_id, library_id=None):
         """
         Returns a dictionary of a given plugin's settings
 
         :param plugin_id:
+        :param library_id:
         :return:
         """
         # Get the path for this plugin
@@ -374,18 +354,18 @@ def get_plugin_settings(self, plugin_id):
 
         try:
             # Settings plugin_settings
-            plugin_settings = plugin_module.Settings()
+            plugin_settings = plugin_module.Settings(library_id=library_id)
 
             all_plugin_settings = copy.deepcopy(plugin_settings.get_setting())
             plugin_form_settings = copy.deepcopy(plugin_settings.get_form_settings())
         except Exception as e:
-            self._log(str(e), level='exception')
+            self._log("Exception while fetching settings for plugin '{}'".format(plugin_id), str(e), level='exception')
             all_plugin_settings = {}
             plugin_form_settings = {}
 
         return all_plugin_settings, plugin_form_settings
 
-    def save_plugin_settings(self, plugin_id, settings):
+    def save_plugin_settings(self, plugin_id, settings, library_id=None):
         """
         Saves a collection of a given plugin's settings.
         Returns a boolean result for the overall success
@@ -393,6 +373,7 @@ def save_plugin_settings(self, plugin_id, settings):
 
         :param plugin_id:
         :param settings:
+        :param library_id:
         :return:
         """
         # Get the path for this plugin
@@ -402,7 +383,7 @@ def save_plugin_settings(self, plugin_id, settings):
         plugin_module = self.__load_plugin_module(plugin_id, plugin_path)
 
         try:
-            plugin_settings = plugin_module.Settings()
+            plugin_settings = plugin_module.Settings(library_id=library_id)
 
             save_result = True
             for key in settings:
@@ -417,9 +398,31 @@ def save_plugin_settings(self, plugin_id, settings):
 
             return save_result
         except Exception as e:
+            self._log("Exception while saving settings for plugin '{}'".format(plugin_id), str(e), level='exception')
             self._log(str(e), level='exception')
             return False
 
+    def reset_plugin_settings(self, plugin_id, library_id=None):
+        """
+        Reset a plugin settings by removing the config file
+
+        :param plugin_id:
+        :param library_id:
+        :return:
+        """
+        # Get the path for this plugin
+        plugin_path = self.__get_plugin_directory(plugin_id)
+
+        # Load this plugin module
+        plugin_module = self.__load_plugin_module(plugin_id, plugin_path)
+
+        try:
+            plugin_settings = plugin_module.Settings(library_id=library_id)
+            return plugin_settings.reset_settings_to_defaults()
+        except Exception as e:
+            self._log("Exception while resetting settings for plugin '{}'".format(plugin_id), str(e), level='exception')
+            return False
+
     def get_plugin_changelog(self, plugin_id):
         """
         Returns a list of lines from the plugin's changelog
diff --git a/unmanic/libs/unplugins/plugin_types/postprocessor/file_move.py b/unmanic/libs/unplugins/plugin_types/postprocessor/file_move.py
index 252b9359..f3f741f4 100644
--- a/unmanic/libs/unplugins/plugin_types/postprocessor/file_move.py
+++ b/unmanic/libs/unplugins/plugin_types/postprocessor/file_move.py
@@ -40,6 +40,7 @@ class FileMove(PluginType):
     Runner function - configures additional postprocessor file movements during the postprocessor stage of a task.
 
     The 'data' object argument includes:
+        library_id              - The library that the current task is associated with
         source_data             - Dictionary containing data pertaining to the original source file.
         remove_source_file      - Boolean, should Unmanic remove the original source file after all copy operations are complete.
         copy_file               - Boolean, should Unmanic run a copy operation with the returned data variables.
diff --git a/unmanic/libs/unplugins/plugin_types/postprocessor/task_result.py b/unmanic/libs/unplugins/plugin_types/postprocessor/task_result.py
index c01f3a40..7f935c58 100644
--- a/unmanic/libs/unplugins/plugin_types/postprocessor/task_result.py
+++ b/unmanic/libs/unplugins/plugin_types/postprocessor/task_result.py
@@ -40,6 +40,7 @@ class TaskResult(PluginType):
     Runner function - provides a means for additional postprocessor functions based on the task success.
 
     The 'data' object argument includes:
+        library_id                      - The library that the current task is associated with
         task_processing_success         - Boolean, did all task processes complete successfully.
         file_move_processes_success     - Boolean, did all postprocessor movement tasks complete successfully.
         destination_files               - List containing all file paths created by postprocessor file movements.
diff --git a/unmanic/libs/unplugins/plugin_types/worker/process_item.py b/unmanic/libs/unplugins/plugin_types/worker/process_item.py
index aa49f8d3..f77f81c3 100644
--- a/unmanic/libs/unplugins/plugin_types/worker/process_item.py
+++ b/unmanic/libs/unplugins/plugin_types/worker/process_item.py
@@ -40,6 +40,7 @@ class ProcessItem(PluginType):
     Runner function - enables additional configured processing jobs during the worker stages of a task.
 
     The 'data' object argument includes:
+        library_id              - The library that the current task is associated with
         exec_command            - A command that Unmanic should execute. Can be empty.
         command_progress_parser - A function that Unmanic can use to parse the STDOUT of the command to collect progress stats. Can be empty.
         file_in                 - The source file to be processed by the command.
diff --git a/unmanic/libs/unplugins/pluginscli.py b/unmanic/libs/unplugins/pluginscli.py
index 8a0d2718..3923dea4 100644
--- a/unmanic/libs/unplugins/pluginscli.py
+++ b/unmanic/libs/unplugins/pluginscli.py
@@ -217,9 +217,13 @@ def create_new_plugins(self):
             "class Settings(PluginSettings):",
             "    settings = {}",
             "",
-            ""
+            "    def __init__(self, *args, **kwargs):",
+            "       super(Settings, self).__init__(*args, **kwargs)",
+            "",
+            "",
         ]
 
+
         # Create runner function template
         runner_template = [
             'def {}(data):'.format(selected_plugin_runner),
diff --git a/unmanic/libs/unplugins/settings.py b/unmanic/libs/unplugins/settings.py
index 62e588ed..77f06192 100644
--- a/unmanic/libs/unplugins/settings.py
+++ b/unmanic/libs/unplugins/settings.py
@@ -37,7 +37,7 @@
 from unmanic.libs.singleton import SingletonType
 
 
-class PluginSettings(object, metaclass=SingletonType):
+class PluginSettings(object):
     """
     A dictionary of settings accessible to the Plugin class and able
     to be configured by users from within the Unmanic WebUI.
@@ -58,7 +58,16 @@ class PluginSettings(object, metaclass=SingletonType):
     """
     settings_configured = None
 
-    def __get_plugin_settings_file(self):
+    """
+    The library ID that we are fetching settings for.
+    
+    """
+    library_id = None
+
+    def __init__(self, *args, **kwargs):
+        self.library_id = kwargs.get('library_id')
+
+    def __get_plugin_settings_file(self, force_library_settings=False):
         plugin_directory = self.get_plugin_directory()
         profile_directory = self.get_profile_directory()
         # Temp code to migrate settings to userdata
@@ -70,7 +79,14 @@ def __get_plugin_settings_file(self):
                     os.path.join(plugin_directory, 'settings.json'),
                     os.path.join(profile_directory, 'settings.json')
                 )
-        return os.path.join(profile_directory, 'settings.json')
+        # If provided with a library ID, then the settings file will be different
+        plugin_settings_file = os.path.join(profile_directory, 'settings.json')
+        if self.library_id:
+            plugin_settings_file = os.path.join(profile_directory, 'settings.{}.json'.format(self.library_id))
+            if not os.path.exists(plugin_settings_file) and not force_library_settings:
+                # If the library file does not yet exist, then resort to using the default settings file
+                plugin_settings_file = os.path.join(profile_directory, 'settings.json')
+        return plugin_settings_file
 
     def __export_configured_settings(self):
         """
@@ -78,7 +94,7 @@ def __export_configured_settings(self):
 
         :return:
         """
-        plugin_settings_file = self.__get_plugin_settings_file()
+        plugin_settings_file = self.__get_plugin_settings_file(force_library_settings=True)
 
         with open(plugin_settings_file, 'w') as f:
             json.dump(self.settings_configured, f, indent=2)
@@ -107,6 +123,22 @@ def __import_configured_settings(self):
             if key in plugin_settings:
                 self.settings_configured[key] = plugin_settings.get(key)
 
+    def reset_settings_to_defaults(self):
+        """
+        Remove all currently configured settings by deleting the settings.json file
+
+        :return:
+        """
+        plugin_settings_file = self.__get_plugin_settings_file()
+
+        # if the file does not yet exist, create it
+        if os.path.exists(plugin_settings_file):
+            os.remove(plugin_settings_file)
+
+        if not os.path.exists(plugin_settings_file):
+            return True
+        return False
+
     def get_plugin_directory(self):
         """
         Return the absolute path to the Plugin's directory.
diff --git a/unmanic/libs/workers.py b/unmanic/libs/workers.py
index 1bce2640..d87516f6 100644
--- a/unmanic/libs/workers.py
+++ b/unmanic/libs/workers.py
@@ -264,10 +264,10 @@ def __exec_worker_runners_on_set_task(self):
 
         :return:
         """
-
         # Init plugins
+        library_id = self.current_task.get_task_library()
         plugin_handler = PluginsHandler()
-        plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type('worker.process_item')
+        plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type('worker.process_item', library_id=library_id)
 
         # Create dictionary of runners info for the frontend
         self.worker_runners_info = {}
@@ -322,6 +322,7 @@ def __exec_worker_runners_on_set_task(self):
 
             # Generate/Reset the data for the runner functions
             data = {
+                "library_id":              library_id,
                 "exec_command":            [],
                 "command_progress_parser": default_progress_parser,
                 "file_in":                 file_in,
diff --git a/unmanic/migrations/001_init_schema_table_settings.py b/unmanic/migrations/001_init_schema_table_settings.py
deleted file mode 100644
index 3014f324..00000000
--- a/unmanic/migrations/001_init_schema_table_settings.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Peewee migrations -- 001_init_schema_table_settings.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import peewee as pw
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """
-    Create the Settings Model
-    """
-
-    @migrator.create_model
-    class Settings(pw.Model):
-        audio_codec = pw.TextField(null=False)
-        audio_stereo_stream_bitrate = pw.TextField(null=False)
-        cache_path = pw.TextField(null=False)
-        config_path = pw.TextField(null=False)
-        debugging = pw.BooleanField(null=False)
-        enable_inotify = pw.BooleanField(null=False)
-        library_path = pw.TextField(null=False)
-        log_path = pw.TextField(null=False)
-        number_of_workers = pw.IntegerField(null=False)
-        out_container = pw.TextField(null=False)
-        remove_subtitle_streams = pw.BooleanField(null=False)
-        run_full_scan_on_start = pw.BooleanField(null=False)
-        schedule_full_scan_minutes = pw.IntegerField(null=False)
-        search_extensions = pw.TextField(null=False)
-        video_codec = pw.TextField(null=False)
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """
-    Remove the Settings Model
-    """
-    migrator.remove_model('settings')
diff --git a/unmanic/migrations/002_expand_application_encoder_settings.py b/unmanic/migrations/002_expand_application_encoder_settings.py
deleted file mode 100644
index 7c4b27f9..00000000
--- a/unmanic/migrations/002_expand_application_encoder_settings.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""Peewee migrations -- 002_expand_application_encoder_settings.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import peewee as pw
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    # Add enable_video_encoding field to Settings Model
-    migrator.add_fields('settings', enable_video_encoding=pw.BooleanField(null=False, default=True))
-    # Add enable_audio_encoding field to Settings Model
-    migrator.add_fields('settings', enable_audio_encoding=pw.BooleanField(null=False, default=True))
-    # Add enable_audio_stream_transcoding field to Settings Model
-    migrator.add_fields('settings', enable_audio_stream_transcoding=pw.BooleanField(null=False, default=True))
-    # Add audio_stream_encoder field to Settings Model
-    migrator.add_fields('settings', audio_stream_encoder=pw.TextField(null=False, default='aac'))
-    # Add enable_audio_stream_stereo_cloning field to Settings Model
-    migrator.add_fields('settings', enable_audio_stream_stereo_cloning=pw.BooleanField(null=False, default=True))
-    # Add audio_codec_cloning field to Settings Model
-    migrator.add_fields('settings', audio_codec_cloning=pw.TextField(null=False, default='aac'))
-    # Add audio_stream_encoder_cloning field to Settings Model
-    migrator.add_fields('settings', audio_stream_encoder_cloning=pw.TextField(null=False, default='aac'))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    # Remove the enable_video_ecoding field from the Settings Model
-    migrator.remove_fields('settings', 'enable_video_ecoding', cascade=True)
-    # Remove the enable_audio_encoding field from the Settings Model
-    migrator.remove_fields('settings', 'enable_audio_encoding', cascade=True)
-    # Remove the enable_audio_stream_transcoding field from the Settings Model
-    migrator.remove_fields('settings', 'enable_audio_stream_transcoding', cascade=True)
-    # Remove the audio_stream_encoder field from the Settings Model
-    migrator.remove_fields('settings', 'audio_stream_encoder', cascade=True)
-    # Remove the enable_audio_stream_stereo_cloning field from the Settings Model
-    migrator.remove_fields('settings', 'enable_audio_stream_stereo_cloning', cascade=True)
-    # Remove the audio_codec_cloning field from the Settings Model
-    migrator.remove_fields('settings', 'audio_codec_cloning', cascade=True)
-    # Remove the audio_stream_encoder_cloning field from the Settings Model
-    migrator.remove_fields('settings', 'audio_stream_encoder_cloning', cascade=True)
diff --git a/unmanic/migrations/003_historical_records.py b/unmanic/migrations/003_historical_records.py
deleted file mode 100644
index 3290eeb3..00000000
--- a/unmanic/migrations/003_historical_records.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""Peewee migrations -- 003_historical_records.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime
-
-import peewee as pw
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """
-    Create the History Models
-    """
-
-    @migrator.create_model
-    class HistoricTasks(pw.Model):
-        task_label = pw.TextField(null=False)
-        task_success = pw.BooleanField(null=False)
-        start_time = pw.DateTimeField(null=False, default=datetime.datetime.now)
-        finish_time = pw.DateTimeField(null=False, default=datetime.datetime.now)
-        processed_by_worker = pw.TextField(null=False)
-
-    @migrator.create_model
-    class HistoricTaskSettings(pw.Model):
-        historictask_id = pw.ForeignKeyField(HistoricTasks)
-        audio_codec = pw.TextField(null=False)
-        audio_stream_encoder = pw.TextField(null=False)
-        audio_codec_cloning = pw.TextField(null=False)
-        audio_stream_encoder_cloning = pw.TextField(null=False)
-        audio_stereo_stream_bitrate = pw.TextField(null=False)
-        cache_path = pw.TextField(null=False)
-        config_path = pw.TextField(null=False)
-        debugging = pw.BooleanField(null=False)
-        enable_audio_encoding = pw.BooleanField(null=False)
-        enable_audio_stream_transcoding = pw.BooleanField(null=False)
-        enable_audio_stream_stereo_cloning = pw.BooleanField(null=False)
-        enable_inotify = pw.BooleanField(null=False)
-        enable_video_encoding = pw.BooleanField(null=False)
-        library_path = pw.TextField(null=False)
-        log_path = pw.TextField(null=False)
-        number_of_workers = pw.IntegerField(null=False)
-        out_container = pw.TextField(null=False)
-        remove_subtitle_streams = pw.BooleanField(null=False)
-        run_full_scan_on_start = pw.BooleanField(null=False)
-        schedule_full_scan_minutes = pw.IntegerField(null=False)
-        search_extensions = pw.TextField(null=False)
-        video_codec = pw.TextField(null=False)
-
-    @migrator.create_model
-    class HistoricTaskProbe(pw.Model):
-        historictask_id = pw.ForeignKeyField(HistoricTasks)
-        type = pw.TextField(null=False, default='source')
-        abspath = pw.TextField(null=False)
-        basename = pw.TextField(null=False)
-        bit_rate = pw.TextField(null=False)
-        format_long_name = pw.TextField(null=False)
-        format_name = pw.TextField(null=False)
-        size = pw.TextField(null=False)
-
-    @migrator.create_model
-    class HistoricTaskProbeStreams(pw.Model):
-        historictaskprobe_id = pw.ForeignKeyField(HistoricTaskProbe)
-        codec_type = pw.TextField(null=False)
-        codec_long_name = pw.TextField(null=False)
-        avg_frame_rate = pw.TextField(null=False)
-        bit_rate = pw.TextField(null=False)
-        coded_height = pw.TextField(null=False)
-        coded_width = pw.TextField(null=False)
-        height = pw.TextField(null=False)
-        width = pw.TextField(null=False)
-        duration = pw.TextField(null=False)
-        channels = pw.TextField(null=False)
-        channel_layout = pw.TextField(null=False)
-
-    @migrator.create_model
-    class HistoricTaskFfmpegLog(pw.Model):
-        historictask_id = pw.ForeignKeyField(HistoricTasks)
-        dump = pw.TextField(null=True)
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """
-    Remove the History Models
-    """
-    migrator.remove_model('historictasks')
-    migrator.remove_model('historictaskstatistics')
diff --git a/unmanic/migrations/004_keep_filename_history.py b/unmanic/migrations/004_keep_filename_history.py
deleted file mode 100644
index 295f922c..00000000
--- a/unmanic/migrations/004_keep_filename_history.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Peewee migrations -- 004_keep_filename_history.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import peewee as pw
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    # Add keep_filename_history field to Settings Model
-    migrator.add_fields('settings', keep_filename_history=pw.BooleanField(default=True, null=False))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    # Remove the keep_filename_history field from the Settings Model
-    migrator.remove_fields('settings', 'keep_filename_history', cascade=True)
diff --git a/unmanic/migrations/005_add_configuration_for_video_encoder.py b/unmanic/migrations/005_add_configuration_for_video_encoder.py
deleted file mode 100644
index cfecda5b..00000000
--- a/unmanic/migrations/005_add_configuration_for_video_encoder.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Peewee migrations -- 002_expand_application_encoder_settings.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import peewee as pw
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    # Add video_stream_encoder field to Settings Model
-    migrator.add_fields('settings', video_stream_encoder=pw.TextField(null=False, default='libx265'))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    # Remove the video_stream_encoder field from the Settings Model
-    migrator.remove_fields('settings', 'video_stream_encoder', cascade=True)
diff --git a/unmanic/migrations/006_add_tasks_to_schema.py b/unmanic/migrations/006_add_tasks_to_schema.py
deleted file mode 100644
index 3cd3426d..00000000
--- a/unmanic/migrations/006_add_tasks_to_schema.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""Peewee migrations -- 006_add_tasks_to_schema.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """
-    Create the Tasks Models
-    """
-
-    @migrator.create_model
-    class Tasks(pw.Model):
-        abspath = pw.TextField(null=False, unique=True)
-        cache_path = pw.TextField(null=True, unique=True)
-        status = pw.TextField(null=False)
-        success = pw.BooleanField(null=True)
-        start_time = pw.DateTimeField(null=True, default=dt.datetime.now)
-        finish_time = pw.DateTimeField(null=True, default=dt.datetime.now)
-        processed_by_worker = pw.TextField(null=True)
-        ffmpeg_log = pw.TextField(null=False, default='')
-
-    @migrator.create_model
-    class TaskSettings(pw.Model):
-        task_id = pw.ForeignKeyField(Tasks, backref='settings', unique=True, on_delete='CASCADE')
-        audio_codec = pw.TextField(null=False)
-        audio_stream_encoder = pw.TextField(null=False)
-        audio_codec_cloning = pw.TextField(null=False)
-        audio_stream_encoder_cloning = pw.TextField(null=False)
-        audio_stereo_stream_bitrate = pw.TextField(null=False)
-        cache_path = pw.TextField(null=False)
-        config_path = pw.TextField(null=False)
-        keep_filename_history = pw.BooleanField(null=False)
-        debugging = pw.BooleanField(null=False)
-        enable_audio_encoding = pw.BooleanField(null=False)
-        enable_audio_stream_transcoding = pw.BooleanField(null=False)
-        enable_audio_stream_stereo_cloning = pw.BooleanField(null=False)
-        enable_inotify = pw.BooleanField(null=False)
-        enable_video_encoding = pw.BooleanField(null=False)
-        library_path = pw.TextField(null=False)
-        log_path = pw.TextField(null=False)
-        number_of_workers = pw.IntegerField(null=False)
-        out_container = pw.TextField(null=False)
-        remove_subtitle_streams = pw.BooleanField(null=False)
-        run_full_scan_on_start = pw.BooleanField(null=False)
-        schedule_full_scan_minutes = pw.IntegerField(null=False)
-        search_extensions = pw.TextField(null=False)
-        video_codec = pw.TextField(null=False)
-        video_stream_encoder = pw.TextField(null=False)
-        video_stream_encoder = pw.TextField(null=False)
-
-    @migrator.create_model
-    class TaskProbe(pw.Model):
-        task_id = pw.ForeignKeyField(Tasks, backref='probe', on_delete='CASCADE')
-        abspath = pw.TextField(null=False)
-        basename = pw.TextField(null=False)
-        bit_rate = pw.TextField(null=False)
-        format_long_name = pw.TextField(null=False)
-        format_name = pw.TextField(null=False)
-        size = pw.TextField(null=False)
-        duration = pw.TextField(null=False)
-
-    @migrator.create_model
-    class TaskProbeStreams(pw.Model):
-        taskprobe_id = pw.ForeignKeyField(TaskProbe, backref='streams', on_delete='CASCADE')
-        codec_type = pw.TextField(null=False)
-        codec_long_name = pw.TextField(null=False)
-        avg_frame_rate = pw.TextField(null=False)
-        bit_rate = pw.TextField(null=False)
-        coded_height = pw.TextField(null=False)
-        coded_width = pw.TextField(null=False)
-        height = pw.TextField(null=False)
-        width = pw.TextField(null=False)
-        duration = pw.TextField(null=False)
-        channels = pw.TextField(null=False)
-        channel_layout = pw.TextField(null=False)
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """
-    Remove the Tasks Models
-    """
-    migrator.remove_model('Tasks', cascade=True)
diff --git a/unmanic/migrations/007_add_advanced_settings.py b/unmanic/migrations/007_add_advanced_settings.py
deleted file mode 100644
index c6baa33a..00000000
--- a/unmanic/migrations/007_add_advanced_settings.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""Peewee migrations -- 007_add_advanced_settings.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Add overwrite_additional_ffmpeg_options field to Settings Model
-    migrator.add_fields('settings', overwrite_additional_ffmpeg_options=pw.BooleanField(null=False, default=False))
-    # Add additional_ffmpeg_options field to Settings Model
-    migrator.add_fields('settings', additional_ffmpeg_options=pw.TextField(null=True, default=''))
-    # Add overwrite_additional_ffmpeg_options field to TaskSettings Model
-    migrator.add_fields('tasksettings', overwrite_additional_ffmpeg_options=pw.BooleanField(null=False, default=False))
-    # Add additional_ffmpeg_options field to TaskSettings Model
-    migrator.add_fields('tasksettings', additional_ffmpeg_options=pw.TextField(null=True, default=''))
-    # Add additional_ffmpeg_options field to HistoricTaskSettings Model
-    migrator.add_fields('historictasksettings', additional_ffmpeg_options=pw.TextField(null=True, default=''))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    # Remove the overwrite_additional_ffmpeg_options field from the Settings Model
-    migrator.remove_fields('settings', 'overwrite_additional_ffmpeg_options', cascade=True)
-    # Remove the additional_ffmpeg_options field from the Settings Model
-    migrator.remove_fields('settings', 'additional_ffmpeg_options', cascade=True)
-    # Remove the overwrite_additional_ffmpeg_options field from the TaskSettings Model
-    migrator.remove_fields('tasksettings', 'overwrite_additional_ffmpeg_options', cascade=True)
-    # Remove the additional_ffmpeg_options field from the TaskSettings Model
-    migrator.remove_fields('tasksettings', 'additional_ffmpeg_options', cascade=True)
-    # Remove the additional_ffmpeg_options field from the HistoricTaskSettings Model
-    migrator.remove_fields('historictasksettings', 'additional_ffmpeg_options', cascade=True)
diff --git a/unmanic/migrations/008_add_hardware_decoding_settings.py b/unmanic/migrations/008_add_hardware_decoding_settings.py
deleted file mode 100644
index 0f910776..00000000
--- a/unmanic/migrations/008_add_hardware_decoding_settings.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""Peewee migrations -- 008_add_hardware_decoding_settings.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Add enable_hardware_accelerated_decoding field to Settings Model
-    migrator.add_fields('settings', enable_hardware_accelerated_decoding=pw.BooleanField(null=False, default=False))
-    # Add enable_hardware_accelerated_decoding field to TaskSettings Model
-    migrator.add_fields('tasksettings', enable_hardware_accelerated_decoding=pw.BooleanField(null=False, default=False))
-    # Add enable_hardware_accelerated_decoding field to HistoricTaskSettings Model
-    migrator.add_fields('historictasksettings',
-                        enable_hardware_accelerated_decoding=pw.BooleanField(null=False, default=False))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    # Remove the enable_hardware_accelerated_decoding field from the Settings Model
-    migrator.remove_fields('settings', 'enable_hardware_accelerated_decoding', cascade=True)
-    # Remove the enable_hardware_accelerated_decoding field from the TaskSettings Model
-    migrator.remove_fields('tasksettings', 'enable_hardware_accelerated_decoding', cascade=True)
-    # Remove the enable_hardware_accelerated_decoding field from the HistoricTaskSettings Model
-    migrator.remove_fields('historictasksettings', 'enable_hardware_accelerated_decoding', cascade=True)
diff --git a/unmanic/migrations/009_update_foreign_key_on_update_cascade.py b/unmanic/migrations/009_update_foreign_key_on_update_cascade.py
deleted file mode 100644
index 3398ffaa..00000000
--- a/unmanic/migrations/009_update_foreign_key_on_update_cascade.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Peewee migrations -- 009_update_foreign_key_on_update_cascade.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels.tasksource import TaskProbe
-from unmanic.libs.unmodels.taskprobestreams import TaskProbeStreams
-from unmanic.libs.unmodels.tasks import Tasks
-from unmanic.libs.unmodels.tasksettings import TaskSettings
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Re-create the Tasks table
-    migrator.remove_model(Tasks, cascade=True)
-    migrator.create_table(Tasks)
-
-    # Re-create the TaskSettings table
-    migrator.remove_model(TaskSettings, cascade=True)
-    migrator.create_table(TaskSettings)
-
-    # Re-create the TaskProbe table
-    migrator.remove_model(TaskProbe, cascade=True)
-    migrator.create_table(TaskProbe)
-
-    # Re-create the TaskProbeStreams table
-    migrator.remove_model(TaskProbeStreams, cascade=True)
-    migrator.create_table(TaskProbeStreams)
-
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-
diff --git a/unmanic/migrations/010_add_pluginrepos_table.py b/unmanic/migrations/010_add_pluginrepos_table.py
deleted file mode 100644
index 3615532d..00000000
--- a/unmanic/migrations/010_add_pluginrepos_table.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Peewee migrations -- 010_add_pluginrepos_table.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels.pluginrepos import PluginRepos
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Create the PluginRepos table
-    migrator.create_table(PluginRepos)
-
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    migrator.remove_model(PluginRepos, cascade=True)
-
diff --git a/unmanic/migrations/011_add_plugins_table.py b/unmanic/migrations/011_add_plugins_table.py
deleted file mode 100644
index 307fbd46..00000000
--- a/unmanic/migrations/011_add_plugins_table.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Peewee migrations -- 011_add_plugins_table.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels.plugins import Plugins
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Create the PluginRepos table
-    migrator.create_table(Plugins)
-
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    migrator.remove_model(Plugins, cascade=True)
-
diff --git a/unmanic/migrations/012_add_installation_table.py b/unmanic/migrations/012_add_installation_table.py
deleted file mode 100644
index 7fe0664d..00000000
--- a/unmanic/migrations/012_add_installation_table.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Peewee migrations -- 012_add_installation_table.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels.installation import Installation
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Create the Installation table
-    migrator.create_table(Installation)
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    migrator.remove_model(Installation, cascade=True)
diff --git a/unmanic/migrations/013_add_update_column_to_plugin_table.py b/unmanic/migrations/013_add_update_column_to_plugin_table.py
deleted file mode 100644
index 264ea24f..00000000
--- a/unmanic/migrations/013_add_update_column_to_plugin_table.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""Peewee migrations -- 013_add_update_column_to_plugin_table.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels import Plugins
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Add enable_hardware_accelerated_decoding field to Plugins Model
-    migrator.create_table(Plugins)
-    # Add keep_original_container field to Settings Model
-    if any(cm for cm in database.get_columns('plugins') if cm.name == 'update_available'):
-        # Remove the current column
-        migrator.remove_fields(Plugins, 'update_available', cascade=True)
-    migrator.add_fields(Plugins, update_available=pw.BooleanField(null=False, default=False))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    migrator.remove_fields(Plugins, 'update_available', cascade=True)
diff --git a/unmanic/migrations/014_add_keep_original_container_to_settings_table.py b/unmanic/migrations/014_add_keep_original_container_to_settings_table.py
deleted file mode 100644
index 0a9de64c..00000000
--- a/unmanic/migrations/014_add_keep_original_container_to_settings_table.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""Peewee migrations -- 014_add_keep_original_container_to_settings_table.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels import Settings, TaskSettings, HistoricTaskSettings
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Add keep_original_container field to Settings Model
-    if any(cm for cm in database.get_columns('settings') if cm.name == 'keep_original_container'):
-        # Remove the current column
-        migrator.remove_fields(Settings, 'keep_original_container', cascade=True)
-    migrator.add_fields(Settings, keep_original_container=pw.BooleanField(null=False, default=False))
-
-    # Add keep_original_container field to TaskSettings Model
-    if any(cm for cm in database.get_columns('tasksettings') if cm.name == 'keep_original_container'):
-        # Remove the current column
-        migrator.remove_fields(TaskSettings, 'keep_original_container', cascade=True)
-    migrator.add_fields(TaskSettings, keep_original_container=pw.BooleanField(null=False, default=False))
-
-    # Add keep_original_container field to HistoricTaskSettings Model
-    if any(cm for cm in database.get_columns('historictasksettings') if cm.name == 'keep_original_container'):
-        # Remove the current column
-        migrator.remove_fields(HistoricTaskSettings, 'keep_original_container', cascade=True)
-    migrator.add_fields(HistoricTaskSettings, keep_original_container=pw.BooleanField(null=False, default=False))
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    # Remove the keep_original_container field from the Settings Model
-    if any(cm for cm in database.get_columns('settings') if cm.name == 'keep_original_container'):
-        migrator.remove_fields(Settings, 'keep_original_container', cascade=True)
-    # Remove the keep_original_container field from the TaskSettings Model
-    if any(cm for cm in database.get_columns('tasksettings') if cm.name == 'keep_original_container'):
-        migrator.remove_fields(TaskSettings, 'keep_original_container', cascade=True)
-    # Remove the keep_original_container field from the HistoricTaskSettings Model
-    if any(cm for cm in database.get_columns('historictasksettings') if cm.name == 'keep_original_container'):
-        migrator.remove_fields(HistoricTaskSettings, 'keep_original_container', cascade=True)
diff --git a/unmanic/migrations/015_add_plugin_flow_table.py b/unmanic/migrations/015_add_plugin_flow_table.py
deleted file mode 100644
index 2b9ef04c..00000000
--- a/unmanic/migrations/015_add_plugin_flow_table.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Peewee migrations -- 015_add_plugin_flow_table.py.
-
-Some examples (model - class or model name)::
-
-    > Model = migrator.orm['model_name']            # Return model in current state by name
-
-    > migrator.sql(sql)                             # Run custom SQL
-    > migrator.python(func, *args, **kwargs)        # Run python code
-    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
-    > migrator.remove_model(model, cascade=True)    # Remove a model
-    > migrator.add_fields(model, **fields)          # Add fields to a model
-    > migrator.change_fields(model, **fields)       # Change fields
-    > migrator.remove_fields(model, *field_names, cascade=True)
-    > migrator.rename_field(model, old_field_name, new_field_name)
-    > migrator.rename_table(model, new_table_name)
-    > migrator.add_index(model, *col_names, unique=False)
-    > migrator.drop_index(model, *col_names)
-    > migrator.add_not_null(model, *field_names)
-    > migrator.drop_not_null(model, *field_names)
-    > migrator.add_default(model, field_name, default)
-
-"""
-
-import datetime as dt
-import peewee as pw
-from decimal import ROUND_HALF_EVEN
-
-from unmanic.libs.unmodels.pluginflow import PluginFlow
-
-try:
-    import playhouse.postgres_ext as pw_pext
-except ImportError:
-    pass
-
-SQL = pw.SQL
-
-
-def migrate(migrator, database, fake=False, **kwargs):
-    """Write your migrations here."""
-    # Create the PluginFlow table
-    migrator.create_table(PluginFlow)
-
-
-def rollback(migrator, database, fake=False, **kwargs):
-    """Write your rollback migrations here."""
-    migrator.remove_model(PluginFlow, cascade=True)
diff --git a/unmanic/migrations/README.md b/unmanic/migrations/README.md
deleted file mode 100644
index 373061bc..00000000
--- a/unmanic/migrations/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Legacy migrations
-
-This directory contains legacy migrations for Unmanic <= 0.1.0.
-
-This will be removed in a future commit.
diff --git a/unmanic/webserver/api_v1/history_api.py b/unmanic/webserver/api_v1/history_api.py
index 6798044a..df64d8fa 100644
--- a/unmanic/webserver/api_v1/history_api.py
+++ b/unmanic/webserver/api_v1/history_api.py
@@ -81,14 +81,6 @@ def fetch_by_id(self, *args, **kwargs):
     def manage_historic_tasks_list(self, *args, **kwargs):
         request_dict = json.loads(self.request.body)
 
-        # Return a list of historical tasks to the pending task list.
-        #   (on success will continue to return the current list of historical tasks)
-        if request_dict.get("customActionName") == "add-to-pending":
-            errors = completed_tasks.add_historic_tasks_to_pending_tasks_list(request_dict.get('id'))
-            if errors:
-                self.write(json.dumps({"success": False}))
-                return
-
         # Delete a list of historical tasks.
         #   (on success will continue to return the current list of historical tasks)
         if request_dict.get("customActionName") == "delete-from-history":
diff --git a/unmanic/webserver/api_v1/plugins_api.py b/unmanic/webserver/api_v1/plugins_api.py
index 081e55fe..fb782edf 100644
--- a/unmanic/webserver/api_v1/plugins_api.py
+++ b/unmanic/webserver/api_v1/plugins_api.py
@@ -45,36 +45,11 @@ class ApiPluginsHandler(BaseApiHandler):
     unmanic_data_queues = None
 
     routes = [
-        {
-            "supported_methods": ["POST"],
-            "call_method":       "manage_installed_plugins_list",
-            "path_pattern":      r"/api/v1/plugins/installed",
-        },
-        {
-            "supported_methods": ["POST"],
-            "call_method":       "get_installed_plugin_flow",
-            "path_pattern":      r"/api/v1/plugins/flow",
-        },
-        {
-            "supported_methods": ["POST"],
-            "call_method":       "set_installed_plugin_flow",
-            "path_pattern":      r"/api/v1/plugins/flow/save",
-        },
         {
             "supported_methods": ["GET"],
             "call_method":       "get_plugin_list",
             "path_pattern":      r"/api/v1/plugins/list",
         },
-        {
-            "supported_methods": ["POST"],
-            "call_method":       "get_plugin_info",
-            "path_pattern":      r"/api/v1/plugins/info",
-        },
-        {
-            "supported_methods": ["POST"],
-            "call_method":       "update_plugin_settings",
-            "path_pattern":      r"/api/v1/plugins/settings/update",
-        },
         {
             "supported_methods": ["POST"],
             "call_method":       "install_plugin_by_id",
@@ -113,39 +88,6 @@ def get(self, path):
     def post(self, path):
         self.action_route()
 
-    def manage_installed_plugins_list(self, *args, **kwargs):
-        request_dict = json.loads(self.request.body)
-
-        plugins = PluginsHandler()
-
-        # Uninstall selected plugins
-        if request_dict.get("customActionName") == "remove-selected-plugins":
-            if not plugins.uninstall_plugins_by_db_table_id(request_dict.get("id")):
-                self.write(json.dumps({"success": False}))
-                return
-
-        # Update selected plugins
-        if request_dict.get("customActionName") == "update-selected-plugins":
-            if not plugins.update_plugins_by_db_table_id(request_dict.get("id")):
-                self.write(json.dumps({"success": False}))
-                return
-
-        # Enable selected plugins
-        if request_dict.get("customActionName") == "enable-selected-plugins":
-            if not plugins.enable_plugin_by_db_table_id(request_dict.get("id")):
-                self.write(json.dumps({"success": False}))
-                return
-
-        # Disable selected plugins
-        if request_dict.get("customActionName") == "disable-selected-plugins":
-            if not plugins.disable_plugin_by_db_table_id(request_dict.get("id")):
-                self.write(json.dumps({"success": False}))
-                return
-
-        # Return a list of plugins based on the request JSON body
-        results = self.prepare_filtered_plugins(request_dict)
-        self.write(json.dumps(results))
-
     def prepare_filtered_plugins(self, request_dict):
         """
         Returns a object of records filtered and sorted
@@ -260,44 +202,6 @@ def update_repos(self, *args, **kwargs):
             # Return failure
             self.write(json.dumps({"success": False}))
 
-    def get_installed_plugin_flow(self, *args, **kwargs):
-        plugin_type = self.get_argument('plugin_type')
-
-        plugin_handler = PluginsHandler()
-        plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type(plugin_type)
-
-        # Only return the data that we need
-        return_plugin_flow = []
-        for plugin_module in plugin_modules:
-            return_plugin_flow.append(
-                {
-                    "plugin_id": plugin_module.get("plugin_id"),
-                    "name":      plugin_module.get("name"),
-                }
-            )
-        self.write(json.dumps({"success": True, "plugin_flow": return_plugin_flow}))
-
-    def set_installed_plugin_flow(self, *args, **kwargs):
-        request_dict = json.loads(self.request.body)
-
-        plugin_type = request_dict.get('plugin_type')
-        if not plugin_type:
-            # Return failure
-            self.write(json.dumps({"success": False, "error": "Missing plugin_type"}))
-            return
-
-        flow = request_dict.get('flow', [])
-
-        plugins = PluginsHandler()
-        success = plugins.set_plugin_flow(plugin_type, flow)
-
-        if success:
-            # Return success
-            self.write(json.dumps({"success": True}))
-        else:
-            # Return failure
-            self.write(json.dumps({"success": False}))
-
     def get_plugin_list(self, *args, **kwargs):
         plugins = PluginsHandler()
         # Fetch a list of plugin data cached locally
@@ -318,81 +222,6 @@ def install_plugin_by_id(self, *args, **kwargs):
             # Return failure
             self.write(json.dumps({"success": False}))
 
-    def __get_plugin_settings(self, plugin_id):
-        """
-        Given a plugin ID , return a list of plugin settings for that plugin
-
-        :param plugin_id:
-        :return:
-        """
-        settings = []
-
-        # Check plugin for settings
-        plugin_executor = PluginExecutor()
-        plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(plugin_id)
-        if plugin_settings:
-            for key in plugin_settings:
-                form_input = {
-                    "key_id":         hashlib.md5(key.encode('utf8')).hexdigest(),
-                    "key":            key,
-                    "value":          plugin_settings.get(key),
-                    "input_type":     None,
-                    "label":          None,
-                    "select_options": [],
-                    "range_options":  {},
-                    "display":        "visible",
-                }
-
-                plugin_setting_meta = plugin_settings_meta.get(key, {})
-
-                # Set input type for form
-                form_input['input_type'] = plugin_setting_meta.get('input_type', None)
-                if not form_input['input_type']:
-                    form_input['input_type'] = "text"
-                    if isinstance(form_input['value'], bool):
-                        form_input['input_type'] = "checkbox"
-
-                # Handle unsupported input types (where they may be supported in future versions of Unmanic)
-                supported_input_types = [
-                    "text",
-                    "textarea",
-                    "select",
-                    "checkbox",
-                    "range",
-                    "browse_directory",
-                ]
-                if form_input['input_type'] not in supported_input_types:
-                    form_input['input_type'] = "text"
-
-                # Set input display options
-                form_input['display'] = plugin_setting_meta.get('display', 'visible')
-
-                # Set input label text
-                form_input['label'] = plugin_setting_meta.get('label', None)
-                if not form_input['label']:
-                    form_input['label'] = key
-
-                # Set options if form input is select
-                if form_input['input_type'] == 'select':
-                    form_input['select_options'] = plugin_setting_meta.get('select_options', [])
-                    if not form_input['select_options']:
-                        # No options are given. Revert back to text input
-                        form_input['input_type'] = 'text'
-
-                # Set options if form input is range
-                if form_input['input_type'] == 'range':
-                    range_options = plugin_setting_meta.get('range_options')
-                    if not range_options:
-                        # No options are given. Revert back to text input
-                        form_input['input_type'] = 'text'
-                    else:
-                        form_input['range_options'] = range_options
-                        if not range_options.get('suffix'):
-                            form_input['range_options']['suffix'] = ''
-
-                settings.append(form_input)
-        return settings
-
     def __get_plugin_changelog(self, plugin_id):
         """
         Given a plugin ID , return a list of lines read from the plugin's changelog
@@ -414,104 +243,3 @@ def __get_plugin_long_description(self, plugin_id):
         # Fetch plugin changelog
         plugin_executor = PluginExecutor()
         return plugin_executor.get_plugin_long_description(plugin_id)
-
-    def __get_plugin_info_and_settings(self, plugin_id):
-        plugins = PluginsHandler()
-
-        plugin_installed = True
-        plugin_results = plugins.get_plugin_list_filtered_and_sorted(plugin_id=plugin_id)
-        if not plugin_results:
-            # This plugin is not installed
-            plugin_installed = False
-
-            # Try to fetch it from the repository
-            plugin_list = plugins.get_installable_plugins_list()
-            for plugin in plugin_list:
-                if plugin.get('plugin_id') == plugin_id:
-                    # Create changelog text from remote changelog text file
-                    plugin['changelog'] = plugins.read_remote_changelog_file(plugin.get('changelog_url'))
-                    # Create list as the 'plugin_results' var above will also have returned a list if any results were found.
-                    plugin_results = [plugin]
-                    break
-
-        # Iterate over plugins and append them to the plugin data
-        plugin_data = {}
-        for plugin_result in plugin_results:
-            # Set params as required in template
-            plugin_data = {
-                'id':          plugin_result.get('id'),
-                'plugin_id':   plugin_result.get('plugin_id'),
-                'icon':        plugin_result.get('icon'),
-                'name':        plugin_result.get('name'),
-                'description': plugin_result.get('description'),
-                'tags':        plugin_result.get('tags'),
-                'author':      plugin_result.get('author'),
-                'version':     plugin_result.get('version'),
-                'changelog':   plugin_result.get('changelog', ''),
-                'settings':    [],
-            }
-            if plugin_installed:
-                plugin_data['settings'] = self.__get_plugin_settings(plugin_result.get('plugin_id'))
-                plugin_data['changelog'] = "".join(self.__get_plugin_changelog(plugin_result.get('plugin_id')))
-                plugin_data['description'] += "\n" + "".join(
-                    self.__get_plugin_long_description(plugin_result.get('plugin_id')))
-            break
-
-        return plugin_data
-
-    def get_plugin_info(self, *args, **kwargs):
-        # Fetch ID of plugin to get Info for
-        plugin_id = self.get_argument('plugin_id')
-
-        # Fetch plugin info (and settings if any)
-        plugin_data = self.__get_plugin_info_and_settings(plugin_id)
-
-        self.write(json.dumps({"success": True, "plugin_info": plugin_data}))
-
-    def update_plugin_settings(self, *args, **kwargs):
-        return_data = {
-            "success": False
-        }
-
-        # Fetch ID of plugin to get Info for
-        plugin_id = self.get_argument('plugin_id')
-
-        # Fetch plugin info (and settings if any)
-        plugin_data = self.__get_plugin_info_and_settings(plugin_id)
-
-        # If no plugin data was found for the posted plugin table ID, then return a failure response
-        if not plugin_data:
-            return return_data
-
-        # Create a dictionary of all posted arguments
-        post_params = {}
-        for k, v in self.request.arguments.items():
-            post_params[k] = v[0].decode("utf-8")
-
-        # Loop over all plugin settings in order to find matches in the posted params
-        settings_to_save = {}
-        for setting in plugin_data.get('settings'):
-            key = setting.get('key')
-            key_id = setting.get('key_id')
-            input_type = setting.get('input_type')
-            # Check if setting is in params
-            if key_id in post_params:
-                post_value = post_params.get(key_id, '')
-                # Check if value should be boolean
-                if input_type == 'checkbox':
-                    post_value = True if post_value.lower() == 'true' else False
-                # Add that to our dictionary of settings to save
-                settings_to_save[key] = post_value
-
-        # If we found settings in the post params that need to be saved, save them...
-        result = False
-        if settings_to_save:
-            plugin_executor = PluginExecutor()
-            saved_all_settings = plugin_executor.save_plugin_settings(plugin_data.get('plugin_id'), settings_to_save)
-            # If the save function was successful
-            if saved_all_settings:
-                # Update settings in plugin data that will be returned
-                plugin_data['settings'] = self.__get_plugin_settings(plugin_id)
-                result = True
-
-        self.write(json.dumps({"success": result, "plugin_info": plugin_data}))
diff --git a/unmanic/webserver/api_v1/session_api.py b/unmanic/webserver/api_v1/session_api.py
index 6ead9785..75fa1396 100644
--- a/unmanic/webserver/api_v1/session_api.py
+++ b/unmanic/webserver/api_v1/session_api.py
@@ -55,6 +55,11 @@ class ApiSessionHandler(BaseApiHandler):
             "call_method":       "get_patreon_login_url",
             "path_pattern":      r"/api/v1/session/unmanic-patreon-login-url",
         },
+        {
+            "supported_methods": ["GET"],
+            "call_method":       "get_github_login_url",
+            "path_pattern":      r"/api/v1/session/unmanic-github-login-url",
+        },
         {
             "supported_methods": ["GET"],
             "call_method":       "get_patreon_page",
@@ -111,6 +116,22 @@ def get_patreon_login_url(self, *args, **kwargs):
             }))
             return
 
+    def get_github_login_url(self, *args, **kwargs):
+        uuid = self.session.get_installation_uuid()
+        github_oauth_url = self.session.get_github_login_url()
+        if not github_oauth_url:
+            self.write(json.dumps({"success": False}))
+            return
+        else:
+            self.write(json.dumps({
+                "success": True,
+                "uuid":    uuid,
+                "data":    {
+                    "url": github_oauth_url,
+                }
+            }))
+            return
+
     def get_patreon_page(self, *args, **kwargs):
         uuid = self.session.get_installation_uuid()
         patreon_sponsor_page_data = self.session.get_patreon_sponsor_page()
diff --git a/unmanic/webserver/api_v2/history_api.py b/unmanic/webserver/api_v2/history_api.py
index b557396a..d7a3c86e 100644
--- a/unmanic/webserver/api_v2/history_api.py
+++ b/unmanic/webserver/api_v2/history_api.py
@@ -39,7 +39,7 @@
 from unmanic.webserver.api_v2.schema.schemas import CompletedTasksLogRequestSchema, CompletedTasksLogSchema, \
     CompletedTasksSchema, \
     RequestHistoryTableDataSchema, \
-    RequestTableUpdateByIdList
+    RequestAddCompletedToPendingTasksSchema, RequestTableUpdateByIdList
 from unmanic.webserver.helpers import completed_tasks
 
 
@@ -228,7 +228,7 @@ def add_completed_tasks_to_pending_list(self):
             content:
                 application/json:
                     schema:
-                        RequestTableUpdateByIdList
+                        RequestAddCompletedToPendingTasksSchema
         responses:
             200:
                 description: 'Successful request; Returns success status'
@@ -262,9 +262,11 @@ def add_completed_tasks_to_pending_list(self):
                             InternalErrorSchema
         """
         try:
-            json_request = self.read_json_request(RequestTableUpdateByIdList())
+            json_request = self.read_json_request(RequestAddCompletedToPendingTasksSchema())
+            id_list = json_request.get('id_list', [])
+            library_id = json_request.get('library_id')
 
-            errors = completed_tasks.add_historic_tasks_to_pending_tasks_list(json_request.get('id_list', []))
+            errors = completed_tasks.add_historic_tasks_to_pending_tasks_list(id_list, library_id=library_id)
             if errors:
                 failed_ids = ''
                 for task_id in errors:
diff --git a/unmanic/webserver/api_v2/pending_api.py b/unmanic/webserver/api_v2/pending_api.py
index aaa3a09b..44aa026d 100644
--- a/unmanic/webserver/api_v2/pending_api.py
+++ b/unmanic/webserver/api_v2/pending_api.py
@@ -148,7 +148,7 @@ def get_pending_tasks(self):
                     "dir":    json_request.get('order_direction', 'desc'),
                 }
             }
-            task_list = pending_tasks.prepare_filtered_pending_tasks(params)
+            task_list = pending_tasks.prepare_filtered_pending_tasks(params, include_library=True)
 
             response = self.build_response(
                 PendingTasksSchema(),
diff --git a/unmanic/webserver/api_v2/plugins_api.py b/unmanic/webserver/api_v2/plugins_api.py
index f085af77..0ac13f3d 100644
--- a/unmanic/webserver/api_v2/plugins_api.py
+++ b/unmanic/webserver/api_v2/plugins_api.py
@@ -37,7 +37,8 @@
 from unmanic.webserver.api_v2.schema.schemas import PluginFlowResultsSchema, PluginReposListResultsSchema, \
     PluginTypesResultsSchema, PluginsDataPanelTypesDataSchema, PluginsDataSchema, PluginsInfoResultsSchema, \
     PluginsInstallableResultsSchema, RequestPluginsByIdSchema, RequestPluginsFlowByPluginTypeSchema, \
-    RequestPluginsInfoSchema, RequestPluginsSettingsSaveSchema, RequestPluginsTableDataSchema, \
+    RequestPluginsInfoSchema, RequestPluginsSettingsResetSchema, RequestPluginsSettingsSaveSchema, \
+    RequestPluginsTableDataSchema, \
     RequestSavingPluginsFlowByPluginTypeSchema, RequestTableUpdateByIdList, RequestUpdatePluginReposListSchema
 from unmanic.webserver.helpers import plugins
 
@@ -84,6 +85,11 @@ class ApiPluginsHandler(BaseApiHandler):
             "supported_methods": ["POST"],
             "call_method":       "update_plugin_settings",
         },
+        {
+            "path_pattern":      r"/plugins/settings/reset",
+            "supported_methods": ["POST"],
+            "call_method":       "reset_plugin_settings",
+        },
         {
             "path_pattern":      r"/plugins/installable",
             "supported_methods": ["GET"],
@@ -216,7 +222,7 @@ def enable_plugins(self):
         """
         Plugins - enable
         ---
-        description: Enable a list of plugins.
+        description: DEPRECATED! Enable a list of plugins.
         requestBody:
             description: Requested list of plugins to enable.
             required: True
@@ -257,15 +263,7 @@ def enable_plugins(self):
                             InternalErrorSchema
         """
         try:
-            json_request = self.read_json_request(RequestTableUpdateByIdList())
-
-            if not plugins.enable_plugins(json_request.get('id_list', []), self.unmanic_data_queues.get('frontend_messages')):
-                self.set_status(self.STATUS_ERROR_INTERNAL, reason="Failed to enable the plugins by their IDs")
-                self.write_error()
-                return
-
-            self.write_success()
-            return
+            raise Exception('Endpoint is deprecated. Plugins are now enabled by assigning them to a library')
         except BaseApiError as bae:
             tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
             return
@@ -277,7 +275,7 @@ def disable_plugins(self):
         """
         Plugins - disable
         ---
-        description: Disable a list of plugins.
+        description: DEPRECATED! Disable a list of plugins.
         requestBody:
             description: Requested list of plugins to disable.
             required: True
@@ -318,15 +316,7 @@ def disable_plugins(self):
                             InternalErrorSchema
         """
         try:
-            json_request = self.read_json_request(RequestTableUpdateByIdList())
-
-            if not plugins.disable_plugins(json_request.get('id_list', [])):
-                self.set_status(self.STATUS_ERROR_INTERNAL, reason="Failed to disable the plugins by their IDs")
-                self.write_error()
-                return
-
-            self.write_success()
-            return
+            raise Exception('Endpoint is deprecated. Plugins are now enabled by assigning them to a library')
         except BaseApiError as bae:
             tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
             return
@@ -338,7 +328,7 @@ def update_plugins(self):
         """
         Plugins - update
         ---
-        description: Update a list of plugins.
+        description: Update a list of plugins given their DB table IDs.
         requestBody:
             description: Requested list of plugins to update.
             required: True
@@ -399,7 +389,7 @@ def remove_plugins(self):
         """
         Plugins - remove
         ---
-        description: Remove a list of plugins.
+        description: Remove a list of plugins given their DB table IDs.
         requestBody:
             description: Requested list of plugins to remove.
             required: True
@@ -462,7 +452,7 @@ def get_plugin_info(self):
         ---
         description: Returns a the metadata and settings of a requested plugin.
         requestBody:
-            description: Requested a single plugin to install.
+            description: Requested a single plugin's info.
             required: True
             content:
                 application/json:
@@ -505,8 +495,11 @@ def get_plugin_info(self):
 
             plugin_id = json_request.get('plugin_id')
             prefer_local = json_request.get('prefer_local')
+            library_id = json_request.get('library_id')
 
-            plugins_info = plugins.prepare_plugin_info_and_settings(plugin_id, prefer_local)
+            plugins_info = plugins.prepare_plugin_info_and_settings(plugin_id,
+                                                                    prefer_local=prefer_local,
+                                                                    library_id=library_id)
 
             response = self.build_response(
                 PluginsInfoResultsSchema(),
@@ -520,7 +513,7 @@ def get_plugin_info(self):
                     "version":     plugins_info.get('version'),
                     "changelog":   plugins_info.get('changelog'),
                     "status":      plugins_info.get('status'),
-                    "settings":    plugins_info.get('settings')
+                    "settings":    plugins_info.get('settings'),
                 }
             )
             self.write_success(response)
@@ -579,7 +572,11 @@ def update_plugin_settings(self):
         try:
             json_request = self.read_json_request(RequestPluginsSettingsSaveSchema())
 
-            if not plugins.update_plugin_settings(json_request.get('plugin_id'), json_request.get('settings')):
+            plugin_id = json_request.get('plugin_id')
+            settings = json_request.get('settings')
+            library_id = json_request.get('library_id')
+
+            if not plugins.update_plugin_settings(plugin_id, settings, library_id=library_id):
                 self.set_status(self.STATUS_ERROR_INTERNAL, reason="Failed to save plugins settings")
                 self.write_error()
                 return
@@ -593,6 +590,70 @@ def update_plugin_settings(self):
             self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
             self.write_error()
 
+    def reset_plugin_settings(self):
+        """
+        Plugins - Reset the settings of a single plugin
+        ---
+        description: Reset the settings of a single plugin.
+        requestBody:
+            description: Requested a plugins settings be reset.
+            required: True
+            content:
+                application/json:
+                    schema:
+                        RequestPluginsSettingsResetSchema
+        responses:
+            200:
+                description: 'Successful request; Returns success status'
+                content:
+                    application/json:
+                        schema:
+                            BaseSuccessSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            json_request = self.read_json_request(RequestPluginsSettingsResetSchema())
+
+            plugin_id = json_request.get('plugin_id')
+            library_id = json_request.get('library_id')
+
+            if not plugins.reset_plugin_settings(plugin_id, library_id=library_id):
+                self.set_status(self.STATUS_ERROR_INTERNAL, reason="Failed to reset plugins settings")
+                self.write_error()
+                return
+
+            self.write_success()
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
+
     def get_installable_plugin_list(self):
         """
         Plugins - Read all installable plugins
@@ -810,7 +871,8 @@ def get_enabled_plugins_flow_by_type(self):
         try:
             json_request = self.read_json_request(RequestPluginsFlowByPluginTypeSchema())
 
-            results = plugins.get_enabled_plugin_flows_for_plugin_type(json_request.get('plugin_type'))
+            results = plugins.get_enabled_plugin_flows_for_plugin_type(json_request.get('plugin_type'),
+                                                                       json_request.get('library_id'))
             response = self.build_response(
                 PluginFlowResultsSchema(),
                 {
@@ -874,6 +936,7 @@ def save_enabled_plugin_flow(self):
             json_request = self.read_json_request(RequestSavingPluginsFlowByPluginTypeSchema())
 
             if not plugins.save_enabled_plugin_flows_for_plugin_type(json_request.get('plugin_type'),
+                                                                     json_request.get('library_id'),
                                                                      json_request.get('plugin_flow')):
                 self.set_status(self.STATUS_ERROR_INTERNAL, reason="Failed to update plugin flow by type")
                 self.write_error()
diff --git a/unmanic/webserver/api_v2/schema/schemas.py b/unmanic/webserver/api_v2/schema/schemas.py
index d0527200..d894680b 100644
--- a/unmanic/webserver/api_v2/schema/schemas.py
+++ b/unmanic/webserver/api_v2/schema/schemas.py
@@ -343,6 +343,16 @@ class CompletedTasksLogSchema(BaseSchema):
     )
 
 
+class RequestAddCompletedToPendingTasksSchema(RequestTableUpdateByIdList):
+    """Schema for adding a completed task to the pending task queue"""
+
+    library_id = fields.Int(
+        required=False,
+        load_default=0,
+        example=1,
+    )
+
+
 # PENDING
 # =======
 
@@ -388,6 +398,16 @@ class PendingTasksTableResultsSchema(BaseSchema):
         description="The uploaded file md5 checksum",
         example="5425ab3df5cdbad2e1099bb4cb963a4f",
     )
+    library_id = fields.Int(
+        required=False,
+        description="The ID of the library for which this task was created",
+        example=1,
+    )
+    library_name = fields.Str(
+        required=False,
+        description="The name of the library for which this task was created",
+        example="Default",
+    )
 
 
 class PendingTasksSchema(TableRecordsSuccessSchema):
@@ -441,11 +461,6 @@ class PluginStatusSchema(BaseSchema):
         description="Is the plugin installed",
         example=True,
     )
-    enabled = fields.Boolean(
-        required=False,
-        description="Is the plugin enabled",
-        example=True,
-    )
     update_available = fields.Boolean(
         required=False,
         description="Does the plugin have an update available",
@@ -515,6 +530,11 @@ class PluginsMetadataResultsSchema(BaseSchema):
         description="The plugin changelog",
         example="[b][color=56adda]0.0.1[/color][/b]• initial version",
     )
+    has_config = fields.Boolean(
+        required=False,
+        description="The plugin has the ability to be configured",
+        example=True,
+    )
 
 
 class PluginsTableResultsSchema(PluginsMetadataResultsSchema):
@@ -547,6 +567,11 @@ class RequestPluginsInfoSchema(RequestPluginsByIdSchema):
         load_default=True,
         example=True,
     )
+    library_id = fields.Int(
+        required=False,
+        load_default=0,
+        example=1,
+    )
 
 
 class PluginsConfigInputItemSchema(BaseSchema):
@@ -632,6 +657,25 @@ class RequestPluginsSettingsSaveSchema(BaseSchema):
         many=True,
         description="The plugin settings",
     )
+    library_id = fields.Int(
+        required=False,
+        load_default=0,
+        example=1,
+    )
+
+
+class RequestPluginsSettingsResetSchema(BaseSchema):
+    """Schema for requesting the reset of a plugins settings by the plugin install ID"""
+
+    plugin_id = fields.Str(
+        required=True,
+        example="encoder_video_hevc_vaapi",
+    )
+    library_id = fields.Int(
+        required=False,
+        load_default=0,
+        example=1,
+    )
 
 
 class PluginsMetadataInstallableResultsSchema(PluginsMetadataResultsSchema):
@@ -694,6 +738,11 @@ class RequestPluginsFlowByPluginTypeSchema(BaseSchema):
         required=True,
         example="library_management.file_test",
     )
+    library_id = fields.Int(
+        required=False,
+        load_default=1,
+        example=1,
+    )
 
 
 class PluginFlowDataResultsSchema(BaseSchema):
@@ -753,6 +802,11 @@ class RequestSavingPluginsFlowByPluginTypeSchema(RequestPluginsFlowByPluginTypeS
         many=True,
         validate=validate.Length(min=1),
     )
+    library_id = fields.Int(
+        required=False,
+        load_default=1,
+        example=1,
+    )
 
 
 class PluginReposMetadataResultsSchema(BaseSchema):
@@ -873,7 +927,6 @@ class SettingsReadAndWriteSchema(BaseSchema):
             "schedule_full_scan_minutes": 1440,
             "follow_symlinks":            True,
             "run_full_scan_on_start":     False,
-            "enable_inotify":             False,
             "number_of_workers":          1,
             "worker_event_schedules":     [
                 {
@@ -955,6 +1008,145 @@ class SettingsRemoteInstallationLinkConfigSchema(BaseSchema):
     )
 
 
+class LibraryResultsSchema(BaseSchema):
+    """Schema for library results"""
+
+    id = fields.Int(
+        required=True,
+        description="",
+        example=1,
+    )
+    name = fields.Str(
+        required=True,
+        description="The name of the library",
+        example="Default",
+    )
+    path = fields.Str(
+        required=True,
+        description="The library path",
+        example="/library",
+    )
+
+
+class SettingsLibrariesListSchema(BaseSchema):
+    """Schema to list all libraries"""
+
+    libraries = fields.Nested(
+        LibraryResultsSchema,
+        required=True,
+        description="Results",
+        many=True,
+        validate=validate.Length(min=1),
+    )
+
+
+class RequestLibraryByIdSchema(BaseSchema):
+    """Schema to request a single library given its ID"""
+
+    id = fields.Int(
+        required=True,
+        description="The ID of the library",
+        example=1,
+    )
+
+
+class SettingsLibraryConfigReadAndWriteSchema(BaseSchema):
+    """Schema to display the data from the remote installation"""
+
+    library_config = fields.Dict(
+        required=True,
+        description="The library configuration",
+        example={
+            "id":             1,
+            "name":           "Default",
+            "path":           "/library",
+            "enable_scanner": False,
+            "enable_inotify": False,
+        },
+    )
+
+    plugins = fields.Dict(
+        required=False,
+        description="The library's enabled plugins",
+        example={
+            "enabled_plugins": [
+                {
+                    "library_id":  1,
+                    "plugin_id":   "notify_plex",
+                    "name":        "Notify Plex",
+                    "description": "Notify Plex on completion of a task.",
+                    "icon":        "https://raw.githubusercontent.com/Josh5/unmanic.plugin.notify_plex/master/icon.png"
+                }
+            ]
+        },
+    )
+
+
+class SettingsLibraryPluginConfigExportSchema(BaseSchema):
+    """Schema for exporting a library's plugin config"""
+
+    plugins = fields.Dict(
+        required=True,
+        description="The library's enabled plugins",
+        example={
+            "enabled_plugins": [
+                {
+                    "library_id":  1,
+                    "plugin_id":   "encoder_audio_ac3",
+                    "name":        "Audio Encoder AC3",
+                    "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                    "icon":        "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                }
+            ],
+            "plugin_flow":     {
+                "library_management.file_test": [
+                    {
+                        "plugin_id":   "encoder_audio_ac3",
+                        "name":        "Audio Encoder AC3",
+                        "author":      "Josh.5",
+                        "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                        "version":     "0.0.2",
+                        "icon":        "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                    }
+                ],
+                "worker.process_item":          [
+                    {
+                        "plugin_id":   "encoder_audio_ac3",
+                        "name":        "Audio Encoder AC3",
+                        "author":      "Josh.5",
+                        "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                        "version":     "0.0.2",
+                        "icon":        "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                    }
+                ],
+                "postprocessor.file_move":      [],
+                "postprocessor.task_result":    []
+            }
+        },
+    )
+
+    library_config = fields.Dict(
+        required=False,
+        description="The library configuration",
+        example={
+            "id":             1,
+            "name":           "Default",
+            "path":           "/library",
+            "enable_scanner": False,
+            "enable_inotify": False,
+        },
+    )
+
+
+class SettingsLibraryPluginConfigImportSchema(SettingsLibraryPluginConfigExportSchema):
+    """Schema for import a library's plugin config"""
+
+    library_id = fields.Int(
+        required=True,
+        example=1,
+    )
+
+
 # VERSION
 # =======
 
diff --git a/unmanic/webserver/api_v2/settings_api.py b/unmanic/webserver/api_v2/settings_api.py
index d13a01d9..8504e81e 100644
--- a/unmanic/webserver/api_v2/settings_api.py
+++ b/unmanic/webserver/api_v2/settings_api.py
@@ -34,12 +34,16 @@
 
 from unmanic import config
 from unmanic.libs.installation_link import Links
+from unmanic.libs.library import Library
 from unmanic.libs.uiserver import UnmanicDataQueues
 from unmanic.webserver.api_v2.base_api_handler import BaseApiError, BaseApiHandler
-from unmanic.webserver.api_v2.schema.schemas import RequestRemoteInstallationLinkConfigSchema, \
-    SettingsReadAndWriteSchema, SettingsRemoteInstallationDataSchema, \
+from unmanic.webserver.api_v2.schema.schemas import RequestLibraryByIdSchema, RequestRemoteInstallationLinkConfigSchema, \
+    SettingsLibrariesListSchema, SettingsLibraryConfigReadAndWriteSchema, SettingsLibraryPluginConfigExportSchema, \
+    SettingsLibraryPluginConfigImportSchema, SettingsReadAndWriteSchema, \
+    SettingsRemoteInstallationDataSchema, \
     SettingsRemoteInstallationLinkConfigSchema, SettingsSystemConfigSchema, \
     RequestSettingsRemoteInstallationAddressValidationSchema
+from unmanic.webserver.helpers import plugins
 
 
 class ApiSettingsHandler(BaseApiHandler):
@@ -78,6 +82,36 @@ class ApiSettingsHandler(BaseApiHandler):
             "supported_methods": ["POST"],
             "call_method":       "write_link_config",
         },
+        {
+            "path_pattern":      r"/settings/libraries",
+            "supported_methods": ["GET"],
+            "call_method":       "get_all_libraries",
+        },
+        {
+            "path_pattern":      r"/settings/library/read",
+            "supported_methods": ["POST"],
+            "call_method":       "read_library_config",
+        },
+        {
+            "path_pattern":      r"/settings/library/write",
+            "supported_methods": ["POST"],
+            "call_method":       "write_library_config",
+        },
+        {
+            "path_pattern":      r"/settings/library/remove",
+            "supported_methods": ["DELETE"],
+            "call_method":       "remove_library",
+        },
+        {
+            "path_pattern":      r"/settings/library/export",
+            "supported_methods": ["POST"],
+            "call_method":       "export_library_plugin_config",
+        },
+        {
+            "path_pattern":      r"/settings/library/import",
+            "supported_methods": ["POST"],
+            "call_method":       "import_library_plugin_config",
+        },
     ]
 
     def initialize(self, **kwargs):
@@ -461,3 +495,430 @@ def write_link_config(self):
         except Exception as e:
             self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
             self.write_error()
+
+    def get_all_libraries(self):
+        """
+        Settings - get list of all libraries
+        ---
+        description: Returns a list of all libraries.
+        responses:
+            200:
+                description: 'Sample response: Returns a list of all libraries.'
+                content:
+                    application/json:
+                        schema:
+                            SettingsLibrariesListSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            libraries = Library.get_all_libraries()
+            response = self.build_response(
+                SettingsLibrariesListSchema(),
+                {
+                    "libraries": libraries,
+                }
+            )
+            self.write_success(response)
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
+
+    def read_library_config(self):
+        """
+        Settings - read the configuration of one library
+        ---
+        description: Read the configuration of one library
+        requestBody:
+            description: The ID of the library
+            required: True
+            content:
+                application/json:
+                    schema:
+                        RequestLibraryByIdSchema
+        responses:
+            200:
+                description: 'Sample response: Returns the remote installation link configuration.'
+                content:
+                    application/json:
+                        schema:
+                            SettingsLibraryConfigReadAndWriteSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            json_request = self.read_json_request(RequestLibraryByIdSchema())
+
+            library_settings = {
+                "library_config": {
+                    "id":             0,
+                    "name":           '',
+                    "path":           '/',
+                    "enable_scanner": False,
+                    "enable_inotify": False,
+                },
+                "plugins":        {
+                    "enabled_plugins": [],
+                }
+            }
+            if json_request.get('id'):
+                # Read the library
+                library_config = Library(json_request.get('id'))
+                library_settings = {
+                    "library_config": {
+                        "id":             library_config.get_id(),
+                        "name":           library_config.get_name(),
+                        "path":           library_config.get_path(),
+                        "enable_scanner": library_config.get_enable_scanner(),
+                        "enable_inotify": library_config.get_enable_inotify(),
+                    },
+                    "plugins":        {
+                        "enabled_plugins": library_config.get_enabled_plugins(),
+                    }
+                }
+
+            response = self.build_response(
+                SettingsLibraryConfigReadAndWriteSchema(),
+                library_settings
+            )
+
+            self.write_success(response)
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
+
+    def write_library_config(self):
+        """
+        Settings - write the configuration of one library
+        ---
+        description: Write the configuration of one library
+        requestBody:
+            description: Requested a dictionary of settings to save.
+            required: True
+            content:
+                application/json:
+                    schema:
+                        SettingsLibraryConfigReadAndWriteSchema
+        responses:
+            200:
+                description: 'Successful request; Returns success status'
+                content:
+                    application/json:
+                        schema:
+                            BaseSuccessSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            json_request = self.read_json_request(SettingsLibraryConfigReadAndWriteSchema())
+
+            # Save settings
+            from unmanic.webserver.helpers import settings
+            library_config = json_request['library_config']
+            plugin_config = json_request.get('plugins', {})
+            library_id = library_config.get('id', 0)
+            settings.save_library_config(library_id, library_config=library_config, plugin_config=plugin_config)
+
+            self.write_success()
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
+
+    def remove_library(self):
+        """
+        Plugins - remove
+        ---
+        description: Remove a library
+        requestBody:
+            description: Requested a library to remove.
+            required: True
+            content:
+                application/json:
+                    schema:
+                        RequestLibraryByIdSchema
+        responses:
+            200:
+                description: 'Successful request; Returns success status'
+                content:
+                    application/json:
+                        schema:
+                            BaseSuccessSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            json_request = self.read_json_request(RequestLibraryByIdSchema())
+
+            # Fetch existing library by ID
+            library = Library(json_request.get('id'))
+
+            # Delete the library
+            if not library.delete():
+                self.set_status(self.STATUS_ERROR_INTERNAL, reason="Failed to remove library by its ID")
+                self.write_error()
+                return
+
+            self.write_success()
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
+
+    def export_library_plugin_config(self):
+        """
+        Settings - export the plugin configuration of one library
+        ---
+        description: Export the plugin configuration of one library
+        requestBody:
+            description: The ID of the library
+            required: True
+            content:
+                application/json:
+                    schema:
+                        RequestLibraryByIdSchema
+        responses:
+            200:
+                description: 'Sample response: Returns the remote installation link configuration.'
+                content:
+                    application/json:
+                        schema:
+                            SettingsLibraryPluginConfigExportSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            json_request = self.read_json_request(RequestLibraryByIdSchema())
+
+            # Read the library
+            library_config = Library(json_request.get('id'))
+
+            # Get list of enabled plugins with their settings
+            enabled_plugins = []
+            for enabled_plugin in library_config.get_enabled_plugins(include_settings=True):
+                enabled_plugins.append({
+                    'plugin_id': enabled_plugin.get('plugin_id'),
+                    'settings':  enabled_plugin.get('settings'),
+                })
+
+            # Create plugin flow
+            plugin_flow = {}
+            for plugin_type in plugins.get_plugin_types_with_flows():
+                plugin_flow[plugin_type] = []
+                flow = plugins.get_enabled_plugin_flows_for_plugin_type(plugin_type, json_request.get('id'))
+                for f in flow:
+                    plugin_flow[plugin_type].append(f.get('plugin_id'))
+
+            plugin_settings = {
+                "plugins":        {
+                    "enabled_plugins": enabled_plugins,
+                    "plugin_flow":     plugin_flow,
+                },
+                "library_config": {
+                    "name":           library_config.get_name(),
+                    "path":           library_config.get_path(),
+                    "enable_scanner": library_config.get_enable_scanner(),
+                    "enable_inotify": library_config.get_enable_inotify(),
+                },
+            }
+
+            response = self.build_response(
+                SettingsLibraryPluginConfigExportSchema(),
+                plugin_settings
+            )
+
+            self.write_success(response)
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
+
+    def import_library_plugin_config(self):
+        """
+        Settings - import the plugin configuration of one library
+        ---
+        description: Import the configuration of one library
+        requestBody:
+            description: Requested a dictionary of settings to save.
+            required: True
+            content:
+                application/json:
+                    schema:
+                        SettingsLibraryPluginConfigImportSchema
+        responses:
+            200:
+                description: 'Successful request; Returns success status'
+                content:
+                    application/json:
+                        schema:
+                            BaseSuccessSchema
+            400:
+                description: Bad request; Check `messages` for any validation errors
+                content:
+                    application/json:
+                        schema:
+                            BadRequestSchema
+            404:
+                description: Bad request; Requested endpoint not found
+                content:
+                    application/json:
+                        schema:
+                            BadEndpointSchema
+            405:
+                description: Bad request; Requested method is not allowed
+                content:
+                    application/json:
+                        schema:
+                            BadMethodSchema
+            500:
+                description: Internal error; Check `error` for exception
+                content:
+                    application/json:
+                        schema:
+                            InternalErrorSchema
+        """
+        try:
+            json_request = self.read_json_request(SettingsLibraryPluginConfigImportSchema())
+
+            # Save settings
+            from unmanic.webserver.helpers import settings
+            library_config = json_request.get('library_config')
+            plugin_config = json_request.get('plugins', {})
+            library_id = json_request.get('library_id')
+            settings.save_library_config(library_id, library_config=library_config, plugin_config=plugin_config)
+
+            self.write_success()
+            return
+        except BaseApiError as bae:
+            tornado.log.app_log.error("BaseApiError.{}: {}".format(self.route.get('call_method'), str(bae)))
+            return
+        except Exception as e:
+            self.set_status(self.STATUS_ERROR_INTERNAL, reason=str(e))
+            self.write_error()
diff --git a/unmanic/webserver/docs/api_schema_v2.json b/unmanic/webserver/docs/api_schema_v2.json
index a4e24fd4..06fcbafb 100644
--- a/unmanic/webserver/docs/api_schema_v2.json
+++ b/unmanic/webserver/docs/api_schema_v2.json
@@ -332,6 +332,28 @@
                     "success"
                 ]
             },
+            "RequestAddCompletedToPendingTasks": {
+                "type": "object",
+                "properties": {
+                    "id_list": {
+                        "type": "array",
+                        "minItems": 1,
+                        "description": "List of table IDs",
+                        "example": [],
+                        "items": {
+                            "type": "integer"
+                        }
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "default": 0,
+                        "example": 1
+                    }
+                },
+                "required": [
+                    "id_list"
+                ]
+            },
             "CompletedTasksLogRequest": {
                 "type": "object",
                 "properties": {
@@ -444,6 +466,16 @@
                         "type": "string",
                         "description": "The uploaded file md5 checksum",
                         "example": "5425ab3df5cdbad2e1099bb4cb963a4f"
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "description": "The ID of the library for which this task was created",
+                        "example": 1
+                    },
+                    "library_name": {
+                        "type": "string",
+                        "description": "The name of the library for which this task was created",
+                        "example": "Default"
                     }
                 },
                 "required": [
@@ -566,11 +598,6 @@
                         "description": "Is the plugin installed",
                         "example": true
                     },
-                    "enabled": {
-                        "type": "boolean",
-                        "description": "Is the plugin enabled",
-                        "example": true
-                    },
                     "update_available": {
                         "type": "boolean",
                         "description": "Does the plugin have an update available",
@@ -629,6 +656,11 @@
                         "description": "The plugin changelog",
                         "example": "[b][color=56adda]0.0.1[/color][/b]• initial version"
                     },
+                    "has_config": {
+                        "type": "boolean",
+                        "description": "The plugin has the ability to be configured",
+                        "example": true
+                    },
                     "id": {
                         "type": "integer",
                         "description": "Item table ID",
@@ -690,6 +722,11 @@
                         "type": "boolean",
                         "default": true,
                         "example": true
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "default": 0,
+                        "example": 1
                     }
                 },
                 "required": [
@@ -817,6 +854,11 @@
                         "description": "The plugin changelog",
                         "example": "[b][color=56adda]0.0.1[/color][/b]• initial version"
                     },
+                    "has_config": {
+                        "type": "boolean",
+                        "description": "The plugin has the ability to be configured",
+                        "example": true
+                    },
                     "settings": {
                         "description": "The plugin settings",
                         "type": "array",
@@ -849,6 +891,11 @@
                         "items": {
                             "$ref": "#/components/schemas/PluginsConfigInputItem"
                         }
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "default": 0,
+                        "example": 1
                     }
                 },
                 "required": [
@@ -856,6 +903,23 @@
                     "settings"
                 ]
             },
+            "RequestPluginsSettingsReset": {
+                "type": "object",
+                "properties": {
+                    "plugin_id": {
+                        "type": "string",
+                        "example": "encoder_video_hevc_vaapi"
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "default": 0,
+                        "example": 1
+                    }
+                },
+                "required": [
+                    "plugin_id"
+                ]
+            },
             "PluginsMetadataInstallableResults": {
                 "type": "object",
                 "properties": {
@@ -907,6 +971,11 @@
                         "description": "The plugin changelog",
                         "example": "[b][color=56adda]0.0.1[/color][/b]• initial version"
                     },
+                    "has_config": {
+                        "type": "boolean",
+                        "description": "The plugin has the ability to be configured",
+                        "example": true
+                    },
                     "package_url": {
                         "type": "string",
                         "description": "The plugin package download URL",
@@ -978,6 +1047,11 @@
                     "plugin_type": {
                         "type": "string",
                         "example": "library_management.file_test"
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "default": 1,
+                        "example": 1
                     }
                 },
                 "required": [
@@ -1055,6 +1129,11 @@
                         "type": "string",
                         "example": "library_management.file_test"
                     },
+                    "library_id": {
+                        "type": "integer",
+                        "default": 1,
+                        "example": 1
+                    },
                     "plugin_flow": {
                         "minItems": 1,
                         "description": "Saved flow",
@@ -1204,7 +1283,6 @@
                             "schedule_full_scan_minutes": 1440,
                             "follow_symlinks": true,
                             "run_full_scan_on_start": false,
-                            "enable_inotify": false,
                             "number_of_workers": 1,
                             "worker_event_schedules": [
                                 {
@@ -1304,6 +1382,215 @@
                     "link_config"
                 ]
             },
+            "LibraryResults": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "description": "",
+                        "example": 1
+                    },
+                    "name": {
+                        "type": "string",
+                        "description": "The name of the library",
+                        "example": "Default"
+                    },
+                    "path": {
+                        "type": "string",
+                        "description": "The library path",
+                        "example": "/library"
+                    }
+                },
+                "required": [
+                    "id",
+                    "name",
+                    "path"
+                ]
+            },
+            "SettingsLibrariesList": {
+                "type": "object",
+                "properties": {
+                    "libraries": {
+                        "minItems": 1,
+                        "description": "Results",
+                        "type": "array",
+                        "items": {
+                            "$ref": "#/components/schemas/LibraryResults"
+                        }
+                    }
+                },
+                "required": [
+                    "libraries"
+                ]
+            },
+            "RequestLibraryById": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "description": "The ID of the library",
+                        "example": 1
+                    }
+                },
+                "required": [
+                    "id"
+                ]
+            },
+            "SettingsLibraryConfigReadAndWrite": {
+                "type": "object",
+                "properties": {
+                    "library_config": {
+                        "type": "object",
+                        "description": "The library configuration",
+                        "example": {
+                            "id": 1,
+                            "name": "Default",
+                            "path": "/library",
+                            "enable_scanner": false,
+                            "enable_inotify": false
+                        }
+                    },
+                    "plugins": {
+                        "type": "object",
+                        "description": "The library's enabled plugins",
+                        "example": {
+                            "enabled_plugins": [
+                                {
+                                    "library_id": 1,
+                                    "plugin_id": "notify_plex",
+                                    "name": "Notify Plex",
+                                    "description": "Notify Plex on completion of a task.",
+                                    "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.notify_plex/master/icon.png"
+                                }
+                            ]
+                        }
+                    }
+                },
+                "required": [
+                    "library_config"
+                ]
+            },
+            "SettingsLibraryPluginConfigExport": {
+                "type": "object",
+                "properties": {
+                    "plugins": {
+                        "type": "object",
+                        "description": "The library's enabled plugins",
+                        "example": {
+                            "enabled_plugins": [
+                                {
+                                    "library_id": 1,
+                                    "plugin_id": "encoder_audio_ac3",
+                                    "name": "Audio Encoder AC3",
+                                    "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                                    "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                                }
+                            ],
+                            "plugin_flow": {
+                                "library_management.file_test": [
+                                    {
+                                        "plugin_id": "encoder_audio_ac3",
+                                        "name": "Audio Encoder AC3",
+                                        "author": "Josh.5",
+                                        "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                                        "version": "0.0.2",
+                                        "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                                    }
+                                ],
+                                "worker.process_item": [
+                                    {
+                                        "plugin_id": "encoder_audio_ac3",
+                                        "name": "Audio Encoder AC3",
+                                        "author": "Josh.5",
+                                        "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                                        "version": "0.0.2",
+                                        "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                                    }
+                                ],
+                                "postprocessor.file_move": [],
+                                "postprocessor.task_result": []
+                            }
+                        }
+                    },
+                    "library_config": {
+                        "type": "object",
+                        "description": "The library configuration",
+                        "example": {
+                            "id": 1,
+                            "name": "Default",
+                            "path": "/library",
+                            "enable_scanner": false,
+                            "enable_inotify": false
+                        }
+                    }
+                },
+                "required": [
+                    "plugins"
+                ]
+            },
+            "SettingsLibraryPluginConfigImport": {
+                "type": "object",
+                "properties": {
+                    "plugins": {
+                        "type": "object",
+                        "description": "The library's enabled plugins",
+                        "example": {
+                            "enabled_plugins": [
+                                {
+                                    "library_id": 1,
+                                    "plugin_id": "encoder_audio_ac3",
+                                    "name": "Audio Encoder AC3",
+                                    "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                                    "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                                }
+                            ],
+                            "plugin_flow": {
+                                "library_management.file_test": [
+                                    {
+                                        "plugin_id": "encoder_audio_ac3",
+                                        "name": "Audio Encoder AC3",
+                                        "author": "Josh.5",
+                                        "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                                        "version": "0.0.2",
+                                        "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                                    }
+                                ],
+                                "worker.process_item": [
+                                    {
+                                        "plugin_id": "encoder_audio_ac3",
+                                        "name": "Audio Encoder AC3",
+                                        "author": "Josh.5",
+                                        "description": "Ensure all audio streams are encoded with the AC3 codec using the native FFmpeg ac3 encoder.",
+                                        "version": "0.0.2",
+                                        "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png"
+                                    }
+                                ],
+                                "postprocessor.file_move": [],
+                                "postprocessor.task_result": []
+                            }
+                        }
+                    },
+                    "library_config": {
+                        "type": "object",
+                        "description": "The library configuration",
+                        "example": {
+                            "id": 1,
+                            "name": "Default",
+                            "path": "/library",
+                            "enable_scanner": false,
+                            "enable_inotify": false
+                        }
+                    },
+                    "library_id": {
+                        "type": "integer",
+                        "example": 1
+                    }
+                },
+                "required": [
+                    "library_id",
+                    "plugins"
+                ]
+            },
             "VersionReadSuccess": {
                 "type": "object",
                 "properties": {
@@ -1772,7 +2059,7 @@
                     "content": {
                         "application/json": {
                             "schema": {
-                                "$ref": "#/components/schemas/RequestTableUpdateByIdList"
+                                "$ref": "#/components/schemas/RequestAddCompletedToPendingTasks"
                             }
                         }
                     }
@@ -2421,7 +2708,7 @@
         },
         "/plugins/enable": {
             "post": {
-                "description": "Enable a list of plugins.",
+                "description": "DEPRECATED! Enable a list of plugins.",
                 "requestBody": {
                     "description": "Requested list of plugins to enable.",
                     "required": true,
@@ -2489,7 +2776,7 @@
         },
         "/plugins/disable": {
             "post": {
-                "description": "Disable a list of plugins.",
+                "description": "DEPRECATED! Disable a list of plugins.",
                 "requestBody": {
                     "description": "Requested list of plugins to disable.",
                     "required": true,
@@ -2557,7 +2844,7 @@
         },
         "/plugins/update": {
             "post": {
-                "description": "Update a list of plugins.",
+                "description": "Update a list of plugins given their DB table IDs.",
                 "requestBody": {
                     "description": "Requested list of plugins to update.",
                     "required": true,
@@ -2625,7 +2912,7 @@
         },
         "/plugins/remove": {
             "delete": {
-                "description": "Remove a list of plugins.",
+                "description": "Remove a list of plugins given their DB table IDs.",
                 "requestBody": {
                     "description": "Requested list of plugins to remove.",
                     "required": true,
@@ -2695,7 +2982,7 @@
             "post": {
                 "description": "Returns a the metadata and settings of a requested plugin.",
                 "requestBody": {
-                    "description": "Requested a single plugin to install.",
+                    "description": "Requested a single plugin's info.",
                     "required": true,
                     "content": {
                         "application/json": {
@@ -2827,6 +3114,74 @@
                 }
             }
         },
+        "/plugins/settings/reset": {
+            "post": {
+                "description": "Reset the settings of a single plugin.",
+                "requestBody": {
+                    "description": "Requested a plugins settings be reset.",
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/RequestPluginsSettingsReset"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful request; Returns success status",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BaseSuccess"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
         "/plugins/installable": {
             "get": {
                 "description": "Returns a list of installable plugins.",
@@ -3884,6 +4239,403 @@
                 }
             }
         },
+        "/settings/libraries": {
+            "get": {
+                "description": "Returns a list of all libraries.",
+                "responses": {
+                    "200": {
+                        "description": "Sample response: Returns a list of all libraries.",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/SettingsLibrariesList"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/settings/library/read": {
+            "post": {
+                "description": "Read the configuration of one library",
+                "requestBody": {
+                    "description": "The ID of the library",
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/RequestLibraryById"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Sample response: Returns the remote installation link configuration.",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/SettingsLibraryConfigReadAndWrite"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/settings/library/write": {
+            "post": {
+                "description": "Write the configuration of one library",
+                "requestBody": {
+                    "description": "Requested a dictionary of settings to save.",
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/SettingsLibraryConfigReadAndWrite"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful request; Returns success status",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BaseSuccess"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/settings/library/remove": {
+            "delete": {
+                "description": "Remove a library",
+                "requestBody": {
+                    "description": "Requested a library to remove.",
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/RequestLibraryById"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful request; Returns success status",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BaseSuccess"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/settings/library/export": {
+            "post": {
+                "description": "Export the plugin configuration of one library",
+                "requestBody": {
+                    "description": "The ID of the library",
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/RequestLibraryById"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Sample response: Returns the remote installation link configuration.",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/SettingsLibraryPluginConfigExport"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/settings/library/import": {
+            "post": {
+                "description": "Import the configuration of one library",
+                "requestBody": {
+                    "description": "Requested a dictionary of settings to save.",
+                    "required": true,
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/SettingsLibraryPluginConfigImport"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful request; Returns success status",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BaseSuccess"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Bad request; Check `messages` for any validation errors",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadRequest"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Bad request; Requested endpoint not found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadEndpoint"
+                                }
+                            }
+                        }
+                    },
+                    "405": {
+                        "description": "Bad request; Requested method is not allowed",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/BadMethod"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Internal error; Check `error` for exception",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/InternalError"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
         "/upload/pending/file": {
             "post": {
                 "description": "Returns a list of pending tasks.",
diff --git a/unmanic/webserver/docs/api_schema_v2.yaml b/unmanic/webserver/docs/api_schema_v2.yaml
index b5897fbd..68da00fc 100644
--- a/unmanic/webserver/docs/api_schema_v2.yaml
+++ b/unmanic/webserver/docs/api_schema_v2.yaml
@@ -212,6 +212,25 @@ components:
       - error
       - messages
       type: object
+    LibraryResults:
+      properties:
+        id:
+          description: ''
+          example: 1
+          type: integer
+        name:
+          description: The name of the library
+          example: Default
+          type: string
+        path:
+          description: The library path
+          example: /library
+          type: string
+      required:
+      - id
+      - name
+      - path
+      type: object
     PendingTasks:
       properties:
         recordsTotal:
@@ -258,6 +277,14 @@ components:
           description: The uploaded file md5 checksum
           example: 5425ab3df5cdbad2e1099bb4cb963a4f
           type: string
+        library_id:
+          description: The ID of the library for which this task was created
+          example: 1
+          type: integer
+        library_name:
+          description: The name of the library for which this task was created
+          example: Default
+          type: string
       required:
       - abspath
       - id
@@ -341,10 +368,6 @@ components:
           description: Is the plugin installed
           example: true
           type: boolean
-        enabled:
-          description: Is the plugin enabled
-          example: true
-          type: boolean
         update_available:
           description: Does the plugin have an update available
           example: true
@@ -488,6 +511,10 @@ components:
           description: The plugin changelog
           example: "[b][color=56adda]0.0.1[/color][/b]\u2022 initial version"
           type: string
+        has_config:
+          description: The plugin has the ability to be configured
+          example: true
+          type: boolean
         settings:
           description: The plugin settings
           items:
@@ -553,6 +580,10 @@ components:
           description: The plugin changelog
           example: "[b][color=56adda]0.0.1[/color][/b]\u2022 initial version"
           type: string
+        has_config:
+          description: The plugin has the ability to be configured
+          example: true
+          type: boolean
         package_url:
           description: The plugin package download URL
           example: https://raw.githubusercontent.com/Unmanic/unmanic-plugins/repo/plugin_id/plugin_id-1.0.0.zip
@@ -618,6 +649,10 @@ components:
           description: The plugin changelog
           example: "[b][color=56adda]0.0.1[/color][/b]\u2022 initial version"
           type: string
+        has_config:
+          description: The plugin has the ability to be configured
+          example: true
+          type: boolean
         id:
           description: Item table ID
           example: 1
@@ -633,6 +668,22 @@ components:
       - tags
       - version
       type: object
+    RequestAddCompletedToPendingTasks:
+      properties:
+        id_list:
+          description: List of table IDs
+          example: &id003 []
+          items:
+            type: integer
+          minItems: 1
+          type: array
+        library_id:
+          default: 0
+          example: 1
+          type: integer
+      required:
+      - id_list
+      type: object
     RequestDirectoryListingData:
       properties:
         current_path:
@@ -673,6 +724,15 @@ components:
           example: desc
           type: string
       type: object
+    RequestLibraryById:
+      properties:
+        id:
+          description: The ID of the library
+          example: 1
+          type: integer
+      required:
+      - id
+      type: object
     RequestPendingTableData:
       properties:
         start:
@@ -706,7 +766,7 @@ components:
       properties:
         id_list:
           description: List of table IDs
-          example: &id003 []
+          example: *id003
           items:
             type: integer
           minItems: 1
@@ -739,6 +799,10 @@ components:
         plugin_type:
           example: library_management.file_test
           type: string
+        library_id:
+          default: 1
+          example: 1
+          type: integer
       required:
       - plugin_type
       type: object
@@ -755,6 +819,22 @@ components:
           default: true
           example: true
           type: boolean
+        library_id:
+          default: 0
+          example: 1
+          type: integer
+      required:
+      - plugin_id
+      type: object
+    RequestPluginsSettingsReset:
+      properties:
+        plugin_id:
+          example: encoder_video_hevc_vaapi
+          type: string
+        library_id:
+          default: 0
+          example: 1
+          type: integer
       required:
       - plugin_id
       type: object
@@ -768,6 +848,10 @@ components:
           items:
             $ref: '#/components/schemas/PluginsConfigInputItem'
           type: array
+        library_id:
+          default: 0
+          example: 1
+          type: integer
       required:
       - plugin_id
       - settings
@@ -815,6 +899,10 @@ components:
         plugin_type:
           example: library_management.file_test
           type: string
+        library_id:
+          default: 1
+          example: 1
+          type: integer
         plugin_flow:
           description: Saved flow
           items:
@@ -897,6 +985,102 @@ components:
       - level
       - uuid
       type: object
+    SettingsLibrariesList:
+      properties:
+        libraries:
+          description: Results
+          items:
+            $ref: '#/components/schemas/LibraryResults'
+          minItems: 1
+          type: array
+      required:
+      - libraries
+      type: object
+    SettingsLibraryConfigReadAndWrite:
+      properties:
+        library_config:
+          description: The library configuration
+          example:
+            enable_inotify: false
+            enable_scanner: false
+            id: 1
+            name: Default
+            path: /library
+          type: object
+        plugins:
+          description: The library's enabled plugins
+          example:
+            enabled_plugins:
+            - description: Notify Plex on completion of a task.
+              icon: https://raw.githubusercontent.com/Josh5/unmanic.plugin.notify_plex/master/icon.png
+              library_id: 1
+              name: Notify Plex
+              plugin_id: notify_plex
+          type: object
+      required:
+      - library_config
+      type: object
+    SettingsLibraryPluginConfigExport:
+      properties:
+        plugins:
+          description: The library's enabled plugins
+          example: &id004
+            enabled_plugins:
+            - description: Ensure all audio streams are encoded with the AC3 codec
+                using the native FFmpeg ac3 encoder.
+              icon: https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png
+              library_id: 1
+              name: Audio Encoder AC3
+              plugin_id: encoder_audio_ac3
+            plugin_flow:
+              library_management.file_test:
+              - author: Josh.5
+                description: Ensure all audio streams are encoded with the AC3 codec
+                  using the native FFmpeg ac3 encoder.
+                icon: https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png
+                name: Audio Encoder AC3
+                plugin_id: encoder_audio_ac3
+                version: 0.0.2
+              postprocessor.file_move: []
+              postprocessor.task_result: []
+              worker.process_item:
+              - author: Josh.5
+                description: Ensure all audio streams are encoded with the AC3 codec
+                  using the native FFmpeg ac3 encoder.
+                icon: https://raw.githubusercontent.com/Josh5/unmanic.plugin.encoder_audio_ac3/master/icon.png
+                name: Audio Encoder AC3
+                plugin_id: encoder_audio_ac3
+                version: 0.0.2
+          type: object
+        library_config:
+          description: The library configuration
+          example: &id005
+            enable_inotify: false
+            enable_scanner: false
+            id: 1
+            name: Default
+            path: /library
+          type: object
+      required:
+      - plugins
+      type: object
+    SettingsLibraryPluginConfigImport:
+      properties:
+        plugins:
+          description: The library's enabled plugins
+          example: *id004
+          type: object
+        library_config:
+          description: The library configuration
+          example: *id005
+          type: object
+        library_id:
+          example: 1
+          type: integer
+      required:
+      - library_id
+      - plugins
+      type: object
     SettingsReadAndWrite:
       properties:
         settings:
@@ -904,7 +1088,6 @@ components:
           example:
             cache_path: /tmp/unmanic
             debugging: false
-            enable_inotify: false
             enable_library_scanner: false
             follow_symlinks: true
             library_path: /library
@@ -1284,7 +1467,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: '#/components/schemas/RequestTableUpdateByIdList'
+              $ref: '#/components/schemas/RequestAddCompletedToPendingTasks'
         description: Requested list of items to reprocess.
         required: true
       responses:
@@ -1676,7 +1859,7 @@ paths:
           description: Internal error; Check `error` for exception
   /plugins/enable:
     post:
-      description: Enable a list of plugins.
+      description: DEPRECATED! Enable a list of plugins.
       requestBody:
         content:
           application/json:
@@ -1717,7 +1900,7 @@ paths:
           description: Internal error; Check `error` for exception
   /plugins/disable:
     post:
-      description: Disable a list of plugins.
+      description: DEPRECATED! Disable a list of plugins.
       requestBody:
         content:
           application/json:
@@ -1758,7 +1941,7 @@ paths:
           description: Internal error; Check `error` for exception
   /plugins/update:
     post:
-      description: Update a list of plugins.
+      description: Update a list of plugins given their DB table IDs.
       requestBody:
         content:
           application/json:
@@ -1799,7 +1982,7 @@ paths:
           description: Internal error; Check `error` for exception
   /plugins/remove:
     delete:
-      description: Remove a list of plugins.
+      description: Remove a list of plugins given their DB table IDs.
       requestBody:
         content:
           application/json:
@@ -1846,7 +2029,7 @@ paths:
           application/json:
             schema:
               $ref: '#/components/schemas/RequestPluginsInfo'
-        description: Requested a single plugin to install.
+        description: Requested a single plugin's info.
         required: true
       responses:
         '200':
@@ -1921,6 +2104,47 @@ paths:
               schema:
                 $ref: '#/components/schemas/InternalError'
           description: Internal error; Check `error` for exception
+  /plugins/settings/reset:
+    post:
+      description: Reset the settings of a single plugin.
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/RequestPluginsSettingsReset'
+        description: Requested a plugins settings be reset.
+        required: true
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BaseSuccess'
+          description: Successful request; Returns success status
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
   /plugins/installable:
     get:
       description: Returns a list of installable plugins.
@@ -2558,6 +2782,245 @@ paths:
               schema:
                 $ref: '#/components/schemas/InternalError'
           description: Internal error; Check `error` for exception
+  /settings/libraries:
+    get:
+      description: Returns a list of all libraries.
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/SettingsLibrariesList'
+          description: 'Sample response: Returns a list of all libraries.'
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
+  /settings/library/read:
+    post:
+      description: Read the configuration of one library
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/RequestLibraryById'
+        description: The ID of the library
+        required: true
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/SettingsLibraryConfigReadAndWrite'
+          description: 'Sample response: Returns the remote installation link configuration.'
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
+  /settings/library/write:
+    post:
+      description: Write the configuration of one library
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/SettingsLibraryConfigReadAndWrite'
+        description: Requested a dictionary of settings to save.
+        required: true
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BaseSuccess'
+          description: Successful request; Returns success status
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
+  /settings/library/remove:
+    delete:
+      description: Remove a library
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/RequestLibraryById'
+        description: Requested a library to remove.
+        required: true
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BaseSuccess'
+          description: Successful request; Returns success status
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
+  /settings/library/export:
+    post:
+      description: Export the plugin configuration of one library
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/RequestLibraryById'
+        description: The ID of the library
+        required: true
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/SettingsLibraryPluginConfigExport'
+          description: 'Sample response: Returns the remote installation link configuration.'
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
+  /settings/library/import:
+    post:
+      description: Import the configuration of one library
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/SettingsLibraryPluginConfigImport'
+        description: Requested a dictionary of settings to save.
+        required: true
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BaseSuccess'
+          description: Successful request; Returns success status
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadRequest'
+          description: Bad request; Check `messages` for any validation errors
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadEndpoint'
+          description: Bad request; Requested endpoint not found
+        '405':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BadMethod'
+          description: Bad request; Requested method is not allowed
+        '500':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/InternalError'
+          description: Internal error; Check `error` for exception
   /upload/pending/file:
     post:
       description: Returns a list of pending tasks.
diff --git a/unmanic/webserver/frontend b/unmanic/webserver/frontend
index 0bd00e3d..e106a8cf 160000
--- a/unmanic/webserver/frontend
+++ b/unmanic/webserver/frontend
@@ -1,1 +1,1 @@
-Subproject commit 0bd00e3df8f2e275037e50e6a591ab1d3115a021
+Subproject commit e106a8cf5de5c933444ce979f1ad2a677a1fc71b
diff --git a/unmanic/webserver/helpers/completed_tasks.py b/unmanic/webserver/helpers/completed_tasks.py
index b06f5842..978dbb34 100644
--- a/unmanic/webserver/helpers/completed_tasks.py
+++ b/unmanic/webserver/helpers/completed_tasks.py
@@ -104,11 +104,12 @@ def remove_completed_tasks(completed_task_ids):
     return task_handler.delete_historic_tasks_recursively(id_list=completed_task_ids)
 
 
-def add_historic_tasks_to_pending_tasks_list(historic_task_ids):
+def add_historic_tasks_to_pending_tasks_list(historic_task_ids, library_id=None):
     """
     Adds a list of historical tasks to the pending tasks list.
 
     :param historic_task_ids:
+    :param library_id:
     :return:
     """
     errors = {}
@@ -129,7 +130,7 @@ def add_historic_tasks_to_pending_tasks_list(historic_task_ids):
         # Create a new task
         new_task = task.Task()
 
-        if not new_task.create_task_by_absolute_path(abspath):
+        if not new_task.create_task_by_absolute_path(abspath, library_id=library_id):
             # If file exists in task queue already this will return false.
             # Do not carry on.
             errors[record.get("id")] = "File already in task queue - '{}'".format(abspath)
diff --git a/unmanic/webserver/helpers/pending_tasks.py b/unmanic/webserver/helpers/pending_tasks.py
index 63d16bdc..a32be77c 100644
--- a/unmanic/webserver/helpers/pending_tasks.py
+++ b/unmanic/webserver/helpers/pending_tasks.py
@@ -32,6 +32,7 @@
 import os
 
 from unmanic.libs import task
+from unmanic.libs.library import Library
 
 
 def prepare_filtered_pending_tasks_for_table(request_dict):
@@ -94,12 +95,13 @@ def prepare_filtered_pending_tasks_for_table(request_dict):
     return return_data
 
 
-def prepare_filtered_pending_tasks(params):
+def prepare_filtered_pending_tasks(params, include_library=False):
     """
     Returns a object of records filtered and sorted
     according to the provided request.
 
     :param params:
+    :param include_library:
     :return:
     """
     start = params.get('start', 0)
@@ -141,6 +143,11 @@ def prepare_filtered_pending_tasks(params):
             'type':     pending_task['type'],
             'status':   pending_task['status'],
         }
+        if include_library:
+            # Get library
+            library = Library(pending_task['library_id'])
+            item['library_id'] = library.get_id()
+            item['library_name'] = library.get_name()
         return_data["results"].append(item)
 
     # Return results
diff --git a/unmanic/webserver/helpers/plugins.py b/unmanic/webserver/helpers/plugins.py
index 3c43095d..58b71e20 100644
--- a/unmanic/webserver/helpers/plugins.py
+++ b/unmanic/webserver/helpers/plugins.py
@@ -56,18 +56,17 @@ def prepare_filtered_plugins(params):
         })
     ]
 
-    enabled = params.get('enabled')
-
     # Fetch Plugins
     plugins = PluginsHandler()
+    plugin_executor = PluginExecutor()
     # Get total count
     records_total_count = plugins.get_total_plugin_list_count()
     # Get quantity after filters (without pagination)
     records_filtered_count = plugins.get_plugin_list_filtered_and_sorted(order=order, start=0, length=0,
-                                                                         search_value=search_value, enabled=enabled).count()
+                                                                         search_value=search_value).count()
     # Get filtered/sorted results
     plugin_results = plugins.get_plugin_list_filtered_and_sorted(order=order, start=start, length=length,
-                                                                 search_value=search_value, enabled=enabled)
+                                                                 search_value=search_value)
 
     # Build return data
     return_data = {
@@ -80,9 +79,13 @@ def prepare_filtered_plugins(params):
     for plugin_result in plugin_results:
         # Set plugin status
         plugin_status = {
-            "enabled":          plugin_result.get('enabled'),
             "update_available": plugin_result.get('update_available'),
         }
+        # Check if plugin is able to be configured
+        has_config = False
+        plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(plugin_result.get('plugin_id'))
+        if plugin_settings:
+            has_config = True
         # Set params as required in template
         item = {
             'id':          plugin_result.get('id'),
@@ -94,6 +97,7 @@ def prepare_filtered_plugins(params):
             'author':      plugin_result.get('author'),
             'version':     plugin_result.get('version'),
             'status':      plugin_status,
+            'has_config':  has_config,
         }
         return_data["results"].append(item)
 
@@ -117,9 +121,16 @@ def get_plugin_types_with_flows():
     return return_plugin_types
 
 
-def get_enabled_plugin_flows_for_plugin_type(plugin_type):
+def get_enabled_plugin_flows_for_plugin_type(plugin_type, library_id):
+    """
+    Fetch all enabled plugin flows for a plugin type
+
+    :param plugin_type:
+    :param library_id:
+    :return:
+    """
     plugin_handler = PluginsHandler()
-    plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type(plugin_type)
+    plugin_modules = plugin_handler.get_enabled_plugin_modules_by_type(plugin_type, library_id=library_id)
 
     # Only return the data that we need
     return_plugin_flow = []
@@ -153,32 +164,17 @@ def exec_plugin_runner(data, plugin_id):
     return plugin_handler.exec_plugin_runner(data, plugin_id, 'frontend.panel')
 
 
-def save_enabled_plugin_flows_for_plugin_type(plugin_type, plugin_flow):
-    plugins = PluginsHandler()
-    return plugins.set_plugin_flow(plugin_type, plugin_flow)
-
-
-def enable_plugins(plugin_table_ids, frontend_messages=None):
+def save_enabled_plugin_flows_for_plugin_type(plugin_type, library_id, plugin_flow):
     """
-    Enables a list of plugins
+    Save a plugin flow given the plugin type and library ID
 
-    :param frontend_messages:
-    :param plugin_table_ids:
+    :param plugin_type:
+    :param library_id:
+    :param plugin_flow:
     :return:
     """
-    plugins_handler = PluginsHandler()
-    return plugins_handler.enable_plugin_by_db_table_id(plugin_table_ids, frontend_messages)
-
-
-def disable_plugins(plugin_table_ids):
-    """
-    Disables a list of plugins
-
-    :param plugin_table_ids:
-    :return:
-    """
-    plugins_handler = PluginsHandler()
-    return plugins_handler.disable_plugin_by_db_table_id(plugin_table_ids)
+    plugins = PluginsHandler()
+    return plugins.set_plugin_flow(plugin_type, library_id, plugin_flow)
 
 
 def remove_plugins(plugin_table_ids):
@@ -203,18 +199,19 @@ def update_plugins(plugin_table_ids):
     return plugins_handler.update_plugins_by_db_table_id(plugin_table_ids)
 
 
-def get_plugin_settings(plugin_id):
+def get_plugin_settings(plugin_id: str, library_id=None):
     """
     Given a plugin installation ID, return a list of plugin settings for that plugin
 
     :param plugin_id:
+    :param library_id:
     :return:
     """
     settings = []
 
     # Check plugin for settings
     plugin_executor = PluginExecutor()
-    plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(plugin_id)
+    plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(plugin_id, library_id=library_id)
     if plugin_settings:
         for key in plugin_settings:
             form_input = {
@@ -306,12 +303,13 @@ def get_plugin_long_description(plugin_id):
     return plugin_executor.get_plugin_long_description(plugin_id)
 
 
-def prepare_plugin_info_and_settings(plugin_id, prefer_local=True):
+def prepare_plugin_info_and_settings(plugin_id, prefer_local=True, library_id=None):
     """
     Returns a object of plugin metadata and current settings for the requested plugin_id
 
     :param prefer_local:
     :param plugin_id:
+    :param library_id:
     :return:
     """
     plugins_handler = PluginsHandler()
@@ -340,7 +338,6 @@ def prepare_plugin_info_and_settings(plugin_id, prefer_local=True):
         # Set plugin status
         plugin_status = {
             "installed":        plugin_result.get('installed', False),
-            "enabled":          plugin_result.get('enabled', False),
             "update_available": plugin_result.get('update_available', False),
         }
         # Set params as required in template
@@ -358,7 +355,7 @@ def prepare_plugin_info_and_settings(plugin_id, prefer_local=True):
             'settings':    [],
         }
         if plugin_installed:
-            plugin_data['settings'] = get_plugin_settings(plugin_result.get('plugin_id'))
+            plugin_data['settings'] = get_plugin_settings(plugin_result.get('plugin_id'), library_id=library_id)
             plugin_data['changelog'] = "".join(get_plugin_changelog(plugin_result.get('plugin_id')))
             plugin_data['description'] += "\n" + "".join(
                 get_plugin_long_description(plugin_result.get('plugin_id')))
@@ -366,10 +363,35 @@ def prepare_plugin_info_and_settings(plugin_id, prefer_local=True):
 
     return plugin_data
 
+def check_if_plugin_is_installed(plugin_id):
+    """
+    Returns true if the given plugin is installed
 
-def update_plugin_settings(plugin_id, settings):
+    :param plugin_id:
+    :return:
+    """
+    plugins_handler = PluginsHandler()
+
+    plugin_installed = True
+    plugin_results = plugins_handler.get_plugin_list_filtered_and_sorted(plugin_id=plugin_id)
+
+    if not plugin_results:
+        # This plugin is not installed
+        plugin_installed = False
+
+    return plugin_installed
+
+def update_plugin_settings(plugin_id, settings, library_id=None):
+    """
+    Updates the settings for the requested plugin_id
+
+    :param plugin_id:
+    :param settings:
+    :param library_id:
+    :return:
+    """
     # Fetch plugin info (and settings if any)
-    plugin_data = prepare_plugin_info_and_settings(plugin_id)
+    plugin_data = prepare_plugin_info_and_settings(plugin_id, library_id=library_id)
 
     # If no plugin data was found for the posted plugin table ID, then return a failure response
     if not plugin_data:
@@ -395,7 +417,9 @@ def update_plugin_settings(plugin_id, settings):
     # If we found settings that need to be saved, save them...
     if settings_to_save:
         plugin_executor = PluginExecutor()
-        saved_all_settings = plugin_executor.save_plugin_settings(plugin_data.get('plugin_id'), settings_to_save)
+        saved_all_settings = plugin_executor.save_plugin_settings(plugin_data.get('plugin_id'),
+                                                                  settings_to_save,
+                                                                  library_id=library_id)
         # If the save function was successful
         if saved_all_settings:
             # Update settings in plugin data that will be returned
@@ -404,6 +428,26 @@ def update_plugin_settings(plugin_id, settings):
     return False
 
 
+def reset_plugin_settings(plugin_id, library_id=None):
+    """
+    Reset a plugin's settings back to defaults (or global config if a library ID is provided)
+
+    :param plugin_id:
+    :param library_id:
+    :return:
+    """
+    # Fetch plugin info (and settings if any)
+    plugin_data = prepare_plugin_info_and_settings(plugin_id, library_id=library_id)
+
+    # If no plugin data was found for the posted plugin table ID, then return a failure response
+    if not plugin_data:
+        return False
+
+    # Reset the plugin settings
+    plugin_executor = PluginExecutor()
+    return plugin_executor.reset_plugin_settings(plugin_data.get('plugin_id'), library_id=library_id)
+
+
 def prepare_installable_plugins_list():
     """
     Return a list of plugins able to be installed.
diff --git a/unmanic/webserver/helpers/settings.py b/unmanic/webserver/helpers/settings.py
new file mode 100644
index 00000000..7c15ee3e
--- /dev/null
+++ b/unmanic/webserver/helpers/settings.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+    unmanic.settings.py
+
+    Written by:               Josh.5 <jsunnex@gmail.com>
+    Date:                     21 Feb 2022, (5:22 PM)
+
+    Copyright:
+           Copyright (C) Josh Sunnex - All Rights Reserved
+
+           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.
+
+"""
+from unmanic.libs.library import Library
+from unmanic.libs.unplugins import PluginExecutor
+from unmanic.webserver.helpers import plugins
+
+
+def save_library_config(library_id, library_config=None, plugin_config=None):
+    """
+    Save a complete library configuration
+
+    :param library_id:
+    :param library_config:
+    :param plugin_config:
+    :return:
+    """
+    # Parse library config
+    if plugin_config is None:
+        plugin_config = {}
+    if library_config is None:
+        library_config = {}
+
+    # Check if this save requires a new library entry
+    if int(library_id) > 0:
+        # Fetch existing library by ID
+        library = Library(library_id)
+    else:
+        # Create a new library
+        library = Library.create(library_config)
+        library_id = library.get_id()
+
+    # Update library config (if the data was given)
+    if library_config:
+        library.set_name(library_config.get('name', library.get_name()))
+        library.set_path(library_config.get('path', library.get_path()))
+        library.set_enable_scanner(library_config.get('enable_scanner', library.get_enable_scanner()))
+        library.set_enable_inotify(library_config.get('enable_inotify', library.get_enable_inotify()))
+
+    # Update enabled plugins (if the data was given)
+    enabled_plugins = plugin_config.get('enabled_plugins')
+    if enabled_plugins is not None:
+        # Ensure plugins are installed (install them if they are not)
+        for ep in enabled_plugins:
+            if not plugins.check_if_plugin_is_installed(ep.get('plugin_id')):
+                plugins.install_plugin_by_id(ep.get('plugin_id'))
+        # Enable the plugins against this library
+        library.set_enabled_plugins(enabled_plugins)
+        # Import settings
+        plugin_executor = PluginExecutor()
+        for ep in enabled_plugins:
+            plugin_executor.save_plugin_settings(ep.get('plugin_id'), ep.get('settings', {}), library_id=library_id)
+
+    # Update plugin flow (if the data was given)
+    plugin_flow = plugin_config.get('plugin_flow')
+    if plugin_flow is not None:
+        for plugin_type in plugins.get_plugin_types_with_flows():
+            flow = []
+            for plugin_id in plugin_flow.get(plugin_type, []):
+                flow.append({'plugin_id': plugin_id})
+            plugins.save_enabled_plugin_flows_for_plugin_type(plugin_type, library_id, flow)
+
+    # Save config
+    return library.save()


Test patch:
diff --git a/tests/integration/test_taskhandler.py b/tests/integration/test_taskhandler.py
index 42fffc2c..3d7e2c97 100644
--- a/tests/integration/test_taskhandler.py
+++ b/tests/integration/test_taskhandler.py
@@ -36,7 +36,6 @@
 
 from tests.support_.test_data import data_queues, mock_jobqueue_class
 from unmanic.libs.taskhandler import TaskHandler
-from unmanic.libs.unmodels import Settings
 from unmanic.libs.unmodels.tasks import Tasks
 
 
@@ -79,7 +78,7 @@ def setup_class(self):
         self.db_connection = Database.select_database(database_settings)
 
         # Create required tables
-        self.db_connection.create_tables([Settings, Tasks])
+        self.db_connection.create_tables([Tasks])
 
         # import config
         from unmanic import config
diff --git a/unmanic/libs/filetest.py b/unmanic/libs/filetest.py
index 5357e984..5f9007cf 100644
--- a/unmanic/libs/filetest.py
+++ b/unmanic/libs/filetest.py
@@ -48,14 +48,16 @@ class FileTest(object):
 
     """
 
-    def __init__(self):
+    def __init__(self, library_id: int):
         self.settings = config.Config()
         unmanic_logging = unlogger.UnmanicLogger.__call__()
         self.logger = unmanic_logging.get_logger(__class__.__name__)
 
         # Init plugins
+        self.library_id = library_id
         self.plugin_handler = PluginsHandler()
-        self.plugin_modules = self.plugin_handler.get_enabled_plugin_modules_by_type('library_management.file_test')
+        self.plugin_modules = self.plugin_handler.get_enabled_plugin_modules_by_type('library_management.file_test',
+                                                                                     library_id=library_id)
 
         # List of filed tasks
         self.failed_paths = []
@@ -135,6 +137,7 @@ def should_file_be_added_to_task_list(self, path):
             # Run tests against plugins
             for plugin_module in self.plugin_modules:
                 data = {
+                    'library_id':                self.library_id,
                     'path':                      path,
                     'issues':                    file_issues.copy(),
                     'add_file_to_pending_tasks': None,
@@ -159,12 +162,13 @@ def should_file_be_added_to_task_list(self, path):
 
 
 class FileTesterThread(threading.Thread):
-    def __init__(self, name, files_to_test, files_to_process, status_updates):
+    def __init__(self, name, files_to_test, files_to_process, status_updates, library_id):
         super(FileTesterThread, self).__init__(name=name)
         self.settings = config.Config()
         self.logger = None
         self.files_to_test = files_to_test
         self.files_to_process = files_to_process
+        self.library_id = library_id
         self.status_updates = status_updates
         self.abort_flag = threading.Event()
         self.abort_flag.clear()
@@ -183,7 +187,7 @@ def run(self):
         # If we have a config set to run a schedule, then start the process.
         # Otherwise close this thread now.
         self._log("Starting {}".format(self.name))
-        file_test = FileTest()
+        file_test = FileTest(self.library_id)
         while not self.abort_flag.is_set():
             try:
                 # Pending task queue has an item available. Fetch it.
diff --git a/unmanic/libs/unplugins/plugin_types/library_management/file_test.py b/unmanic/libs/unplugins/plugin_types/library_management/file_test.py
index fc8bc715..b78b873b 100644
--- a/unmanic/libs/unplugins/plugin_types/library_management/file_test.py
+++ b/unmanic/libs/unplugins/plugin_types/library_management/file_test.py
@@ -40,6 +40,7 @@ class FileTest(PluginType):
     Runner function - enables additional actions during the library management file tests.
 
     The 'data' object argument includes:
+        library_id                      - The library that the current task is associated with
         path                            - String containing the full path to the file being tested.
         issues                          - List of currently found issues for not processing the file.
         add_file_to_pending_tasks       - Boolean, is the file currently marked to be added to the queue for processing.